From 245c04367b79eaad574db9f0583c618af0e3a24d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 8 Jul 2021 23:57:42 +1000 Subject: [PATCH 01/15] Refactor BuildOrderEdit form --- InvenTree/InvenTree/metadata.py | 7 ++- InvenTree/build/serializers.py | 3 ++ .../build/templates/build/build_base.html | 24 +++++++++-- InvenTree/build/urls.py | 1 - InvenTree/build/views.py | 43 ------------------- InvenTree/templates/js/forms.js | 4 ++ InvenTree/templates/js/model_renderers.js | 12 ++++++ 7 files changed, 45 insertions(+), 49 deletions(-) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index f1b1c0c040..92948653b0 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -166,7 +166,12 @@ class InvenTreeMetadata(SimpleMetadata): if model: # Mark this field as "related", and point to the URL where we can get the data! field_info['type'] = 'related field' - field_info['api_url'] = model.get_api_url() field_info['model'] = model._meta.model_name + # Special case for 'user' model + if field_info['model'] == 'user': + field_info['api_url'] = '/api/user/' + else: + field_info['api_url'] = model.get_api_url() + return field_info diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 363614035a..15c0fad84f 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -75,9 +75,11 @@ class BuildSerializer(InvenTreeModelSerializer): 'pk', 'url', 'title', + 'batch', 'creation_date', 'completed', 'completion_date', + 'destination', 'part', 'part_detail', 'overdue', @@ -87,6 +89,7 @@ class BuildSerializer(InvenTreeModelSerializer): 'status', 'status_text', 'target_date', + 'take_from', 'notes', 'link', 'issued_by', diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 2376daf0cf..4fe24d7caf 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -196,10 +196,26 @@ src="{% static 'img/blank_image.png' %}" }); $("#build-edit").click(function () { - launchModalForm("{% url 'build-edit' build.id %}", - { - reload: true - }); + + constructForm('{% url "api-build-detail" build.pk %}', { + fields: { + reference: { + prefix: "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}", + }, + title: {}, + part: {}, + quantity: {}, + batch: {}, + target_date: {}, + take_from: {}, + destination: {}, + link: {}, + issued_by: {}, + responsible: {}, + }, + title: '{% trans "Edit Build Order" %}', + reload: true, + }); }); $("#build-cancel").click(function() { diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 549a20ee7e..588e44fb35 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -7,7 +7,6 @@ from django.conf.urls import url, include from . import views build_detail_urls = [ - url(r'^edit/', views.BuildUpdate.as_view(), name='build-edit'), url(r'^allocate/', views.BuildAllocate.as_view(), name='build-allocate'), url(r'^cancel/', views.BuildCancel.as_view(), name='build-cancel'), url(r'^delete/', views.BuildDelete.as_view(), name='build-delete'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index 16004dacc1..e94cc869db 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -744,49 +744,6 @@ class BuildCreate(AjaxCreateView): pass -class BuildUpdate(AjaxUpdateView): - """ View for editing a Build object """ - - model = Build - form_class = forms.EditBuildForm - context_object_name = 'build' - ajax_form_title = _('Edit Build Order Details') - ajax_template_name = 'modal_form.html' - - def get_form(self): - - form = super().get_form() - - build = self.get_object() - - # Fields which are included in the form, but hidden - hidden = [ - 'parent', - 'sales_order', - ] - - if build.is_complete: - # Fields which cannot be edited once the build has been completed - - hidden += [ - 'part', - 'quantity', - 'batch', - 'take_from', - 'destination', - ] - - for field in hidden: - form.fields[field].widget = HiddenInput() - - return form - - def get_data(self): - return { - 'info': _('Edited build'), - } - - class BuildDelete(AjaxDeleteView): """ View to delete a build """ diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index ac3bcefd04..e282499220 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -1206,6 +1206,10 @@ function renderModelData(name, model, data, parameters, options) { break; case 'owner': renderer = renderOwner; + break; + case 'user': + renderer = renderUser; + break; default: break; } diff --git a/InvenTree/templates/js/model_renderers.js b/InvenTree/templates/js/model_renderers.js index 5b838f184b..3a5b85adac 100644 --- a/InvenTree/templates/js/model_renderers.js +++ b/InvenTree/templates/js/model_renderers.js @@ -92,6 +92,18 @@ function renderPart(name, data, parameters, options) { return html; } +// Renderer for "User" model +function renderUser(name, data, parameters, options) { + + var html = `${data.username}`; + + if (data.first_name && data.last_name) { + html += ` - ${data.first_name} ${data.last_name}`; + } + + return html; +} + // Renderer for "Owner" model function renderOwner(name, data, parameters, options) { From 5016d44b83ed33741a3dd95b1e4e2083b34fee11 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 00:15:49 +1000 Subject: [PATCH 02/15] Add default value for BuildOrder reference --- .../migrations/0030_alter_build_reference.py | 20 +++++++++++++ InvenTree/build/models.py | 30 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 InvenTree/build/migrations/0030_alter_build_reference.py diff --git a/InvenTree/build/migrations/0030_alter_build_reference.py b/InvenTree/build/migrations/0030_alter_build_reference.py new file mode 100644 index 0000000000..75f43c77dc --- /dev/null +++ b/InvenTree/build/migrations/0030_alter_build_reference.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.4 on 2021-07-08 14:14 + +import InvenTree.validators +import build.models +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('build', '0029_auto_20210601_1525'), + ] + + operations = [ + migrations.AlterField( + model_name='build', + name='reference', + field=models.CharField(default=build.models.get_next_build_number, help_text='Build Order Reference', max_length=64, unique=True, validators=[InvenTree.validators.validate_build_order_reference], verbose_name='Reference'), + ), + ] diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 848d774d1c..f1190a6346 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -37,6 +37,35 @@ from part import models as PartModels from users import models as UserModels +def get_next_build_number(): + """ + Returns the next available BuildOrder reference number + """ + + if Build.objects.count() == 0: + return + + build = Build.objects.exclude(reference=None).last() + + attempts = set([build.reference]) + + reference = build.reference + + while 1: + reference = increment(build.reference) + + if reference in attempts: + # Escape infinite recursion + return reference + + if Build.objects.filter(reference=reference).exists(): + attempts.add(reference) + else: + break + + return reference + + class Build(MPTTModel): """ A Build object organises the creation of new StockItem objects from other existing StockItem objects. @@ -130,6 +159,7 @@ class Build(MPTTModel): blank=False, help_text=_('Build Order Reference'), verbose_name=_('Reference'), + default=get_next_build_number, validators=[ validate_build_order_reference ] From 004b36b1dfa9d1622912c8b2126bb0e6f74d6e2f Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 00:18:03 +1000 Subject: [PATCH 03/15] Refactor BuildOrderCreate form --- .../build/templates/build/build_base.html | 21 +---- InvenTree/build/tests.py | 17 ---- InvenTree/build/urls.py | 2 - InvenTree/build/views.py | 77 ------------------- InvenTree/order/models.py | 8 +- InvenTree/part/templates/part/build.html | 4 +- InvenTree/templates/js/build.js | 71 +++++++++++------ 7 files changed, 56 insertions(+), 144 deletions(-) diff --git a/InvenTree/build/templates/build/build_base.html b/InvenTree/build/templates/build/build_base.html index 4fe24d7caf..ece6de36bb 100644 --- a/InvenTree/build/templates/build/build_base.html +++ b/InvenTree/build/templates/build/build_base.html @@ -196,26 +196,7 @@ src="{% static 'img/blank_image.png' %}" }); $("#build-edit").click(function () { - - constructForm('{% url "api-build-detail" build.pk %}', { - fields: { - reference: { - prefix: "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}", - }, - title: {}, - part: {}, - quantity: {}, - batch: {}, - target_date: {}, - take_from: {}, - destination: {}, - link: {}, - issued_by: {}, - responsible: {}, - }, - title: '{% trans "Edit Build Order" %}', - reload: true, - }); + editBuildOrder({{ build.pk }}); }); $("#build-cancel").click(function() { diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index 9a440e0b93..9c5134cc66 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -252,23 +252,6 @@ class TestBuildViews(TestCase): self.assertIn(build.title, content) - def test_build_create(self): - """ Test the build creation view (ajax form) """ - - url = reverse('build-create') - - # Create build without specifying part - response = self.client.get(url, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Create build with valid part - response = self.client.get(url, {'part': 1}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - - # Create build with invalid part - response = self.client.get(url, {'part': 9999}, HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertEqual(response.status_code, 200) - def test_build_allocate(self): """ Test the part allocation view for a Build """ diff --git a/InvenTree/build/urls.py b/InvenTree/build/urls.py index 588e44fb35..c354a17ac7 100644 --- a/InvenTree/build/urls.py +++ b/InvenTree/build/urls.py @@ -35,8 +35,6 @@ build_urls = [ url('^new/', views.BuildItemCreate.as_view(), name='build-item-create'), ])), - url(r'new/', views.BuildCreate.as_view(), name='build-create'), - url(r'^(?P\d+)/', include(build_detail_urls)), url(r'.*$', views.BuildIndex.as_view(), name='build-index'), diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index e94cc869db..2bae825b0f 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -667,83 +667,6 @@ class BuildAllocate(InvenTreeRoleMixin, DetailView): return context -class BuildCreate(AjaxCreateView): - """ - View to create a new Build object - """ - - model = Build - context_object_name = 'build' - form_class = forms.EditBuildForm - ajax_form_title = _('New Build Order') - ajax_template_name = 'modal_form.html' - - def get_form(self): - form = super().get_form() - - if form['part'].value(): - form.fields['part'].widget = HiddenInput() - - return form - - def get_initial(self): - """ Get initial parameters for Build creation. - - If 'part' is specified in the GET query, initialize the Build with the specified Part - """ - - initials = super(BuildCreate, self).get_initial().copy() - - initials['parent'] = self.request.GET.get('parent', None) - - # User has provided a SalesOrder ID - initials['sales_order'] = self.request.GET.get('sales_order', None) - - initials['quantity'] = self.request.GET.get('quantity', 1) - - part = self.request.GET.get('part', None) - - if part: - - try: - part = Part.objects.get(pk=part) - # User has provided a Part ID - initials['part'] = part - initials['destination'] = part.get_default_location() - - to_order = part.quantity_to_order - - if to_order < 1: - to_order = 1 - - initials['quantity'] = to_order - except (ValueError, Part.DoesNotExist): - pass - - initials['reference'] = Build.getNextBuildNumber() - - # Pre-fill the issued_by user - initials['issued_by'] = self.request.user - - return initials - - def get_data(self): - return { - 'success': _('Created new build'), - } - - def validate(self, build, form, **kwargs): - """ - Perform extra form validation. - - - If part is trackable, check that either batch or serial numbers are calculated - - By this point form.is_valid() has been executed - """ - - pass - - class BuildDelete(AjaxDeleteView): """ View to delete a build """ diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py index 22f70d0f3c..99c71d1c3e 100644 --- a/InvenTree/order/models.py +++ b/InvenTree/order/models.py @@ -43,8 +43,10 @@ def get_next_po_number(): attempts = set([order.reference]) + reference = order.reference + while 1: - reference = increment(order.reference) + reference = increment(reference) if reference in attempts: # Escape infinite recursion @@ -70,8 +72,10 @@ def get_next_so_number(): attempts = set([order.reference]) + reference = order.reference + while 1: - reference = increment(order.reference) + reference = increment(reference) if reference in attempts: # Escape infinite recursion diff --git a/InvenTree/part/templates/part/build.html b/InvenTree/part/templates/part/build.html index 4c28bef3d4..29f32c770a 100644 --- a/InvenTree/part/templates/part/build.html +++ b/InvenTree/part/templates/part/build.html @@ -34,9 +34,7 @@ {{ block.super }} $("#start-build").click(function() { newBuildOrder({ - data: { - part: {{ part.id }}, - } + part: {{ part.pk }}, }); }); diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index 67961d1b73..55bd7172e1 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -1,34 +1,59 @@ {% load i18n %} {% load inventree_extras %} + +function buildFormFields() { + return { + reference: { + prefix: "{% settings_value 'BUILDORDER_REFERENCE_PREFIX' %}", + }, + title: {}, + part: {}, + quantity: {}, + batch: {}, + target_date: {}, + take_from: {}, + destination: {}, + link: { + icon: 'fa-link', + }, + issued_by: { + icon: 'fa-user', + }, + responsible: { + icon: 'fa-users', + }, + }; +} + + +function editBuildOrder(pk, options={}) { + + var fields = buildFormFields(); + + constructForm(`/api/build/${pk}/`, { + fields: fields, + reload: true, + title: '{% trans "Edit Build Order" %}', + }); +} + function newBuildOrder(options={}) { /* Launch modal form to create a new BuildOrder. */ - launchModalForm( - "{% url 'build-create' %}", - { - follow: true, - data: options.data || {}, - callback: [ - { - field: 'part', - action: function(value) { - inventreeGet( - `/api/part/${value}/`, {}, - { - success: function(response) { + var fields = buildFormFields(); - //enableField('serial_numbers', response.trackable); - //clearField('serial_numbers'); - } - } - ); - }, - } - ], - } - ) + if (options.part) { + fields.part.value = options.part; + } + + constructForm(`/api/build/`, { + fields: fields, + follow: true, + method: 'POST', + title: '{% trans "Create Build Order" %}' + }); } From 13ca076f42b288ef4bc1ab2c5ab07a3a8ce56b59 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 00:29:36 +1000 Subject: [PATCH 04/15] Fix for form rendering of "required" fields with a default value - Force the "required" parameter to be set --- InvenTree/InvenTree/metadata.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index 92948653b0..c22b39dc43 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -153,6 +153,11 @@ class InvenTreeMetadata(SimpleMetadata): if 'default' not in field_info and not field.default == empty: field_info['default'] = field.get_default() + # Force non-nullable fields to read as "required" + # (even if there is a default value!) + if not field.allow_null and not (hasattr(field, 'allow_blank') and field.allow_blank): + field_info['required'] = True + # Introspect writable related fields if field_info['type'] == 'field' and not field_info['read_only']: From be6ecd958770c404d38b4d7d368eaf6c2d3bdd6d Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 00:36:54 +1000 Subject: [PATCH 05/15] Fixes for BuildOrder forms --- InvenTree/build/serializers.py | 1 + InvenTree/templates/js/build.js | 35 +++++++++++++---------- InvenTree/templates/js/forms.js | 3 ++ InvenTree/templates/js/model_renderers.js | 21 ++++++++++++++ 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 15c0fad84f..0677320401 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -80,6 +80,7 @@ class BuildSerializer(InvenTreeModelSerializer): 'completed', 'completion_date', 'destination', + 'parent', 'part', 'part_detail', 'overdue', diff --git a/InvenTree/templates/js/build.js b/InvenTree/templates/js/build.js index 55bd7172e1..e284a0e8c8 100644 --- a/InvenTree/templates/js/build.js +++ b/InvenTree/templates/js/build.js @@ -10,6 +10,11 @@ function buildFormFields() { title: {}, part: {}, quantity: {}, + parent: { + filters: { + part_detail: true, + } + }, batch: {}, target_date: {}, take_from: {}, @@ -48,6 +53,14 @@ function newBuildOrder(options={}) { fields.part.value = options.part; } + if (options.quantity) { + fields.quantity.value = options.quantity; + } + + if (options.parent) { + fields.parent.value = options.parent; + } + constructForm(`/api/build/`, { fields: fields, follow: true, @@ -409,14 +422,10 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { var idx = $(this).closest('tr').attr('data-index'); var row = $(table).bootstrapTable('getData')[idx]; - // Launch form to create a new build order - launchModalForm('{% url "build-create" %}', { - follow: true, - data: { - part: pk, - parent: buildId, - quantity: requiredQuantity(row) - sumAllocations(row), - } + newBuildOrder({ + part: pk, + parent: buildId, + quantity: requiredQuantity(row) - sumAllocations(row), }); }); @@ -1117,13 +1126,9 @@ function loadBuildPartsTable(table, options={}) { var idx = $(this).closest('tr').attr('data-index'); var row = $(table).bootstrapTable('getData')[idx]; - // Launch form to create a new build order - launchModalForm('{% url "build-create" %}', { - follow: true, - data: { - part: pk, - parent: options.build, - } + newBuildOrder({ + part: pk, + parent: options.build, }); }); } diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index e282499220..4af3a6f45e 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -1204,6 +1204,9 @@ function renderModelData(name, model, data, parameters, options) { case 'supplierpart': renderer = renderSupplierPart; break; + case 'build': + renderer = renderBuild; + break; case 'owner': renderer = renderOwner; break; diff --git a/InvenTree/templates/js/model_renderers.js b/InvenTree/templates/js/model_renderers.js index 3a5b85adac..91736e0b5a 100644 --- a/InvenTree/templates/js/model_renderers.js +++ b/InvenTree/templates/js/model_renderers.js @@ -70,6 +70,27 @@ function renderStockLocation(name, data, parameters, options) { } +function renderBuild(name, data, parameters, options) { + + var image = ''; + + if (data.part_detail && data.part_detail.thumbnail) { + image = data.part_detail.thumbnail; + } else { + image = `/static/img/blank_image.png`; + } + + var html = ``; + + html += `${data.reference} - ${data.quantity} x ${data.part_detail.full_name}`; + html += `{% trans "Build ID" %}: ${data.pk}`; + + html += `

${data.title}

`; + + return html; +} + + // Renderer for "Part" model function renderPart(name, data, parameters, options) { From bec98d355a08c6fdc09abaf3c73de0639a8fd2fe Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 00:49:19 +1000 Subject: [PATCH 06/15] Improvements - part_detail defaults to True for BuildSerializer - Handle invalid parent for BuildOrder --- InvenTree/build/models.py | 12 +++++++++++- InvenTree/build/serializers.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index f1190a6346..774e606040 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -21,6 +21,7 @@ from django.core.validators import MinValueValidator from markdownx.models import MarkdownxField from mptt.models import MPTTModel, TreeForeignKey +from mptt.exceptions import InvalidMove from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode @@ -89,11 +90,20 @@ class Build(MPTTModel): responsible: User (or group) responsible for completing the build """ + OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) + @staticmethod def get_api_url(): return reverse('api-build-list') - OVERDUE_FILTER = Q(status__in=BuildStatus.ACTIVE_CODES) & ~Q(target_date=None) & Q(target_date__lte=datetime.now().date()) + def save(self, *args, **kwargs): + + try: + super().save(*args, **kwargs) + except InvalidMove: + raise ValidationError({ + 'parent': _('Invalid choice for parent build'), + }) class Meta: verbose_name = _("Build Order") diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 0677320401..5c0fced884 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -62,7 +62,7 @@ class BuildSerializer(InvenTreeModelSerializer): return queryset def __init__(self, *args, **kwargs): - part_detail = kwargs.pop('part_detail', False) + part_detail = kwargs.pop('part_detail', True) super().__init__(*args, **kwargs) From 7e7fe7d63f81b27ce57c02844daf507f2aa8d3f8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 00:58:17 +1000 Subject: [PATCH 07/15] Refactor buildlist filtering --- InvenTree/build/api.py | 66 +++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 30 deletions(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 904a523a09..ee68dd0c60 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -5,11 +5,14 @@ JSON API for the Build app # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django_filters.rest_framework import DjangoFilterBackend +from django.conf.urls import url, include +from django.db.models import query + from rest_framework import filters from rest_framework import generics -from django.conf.urls import url, include +from django_filters.rest_framework import DjangoFilterBackend +from django_filters import rest_framework as rest_filters from InvenTree.api import AttachmentMixin from InvenTree.helpers import str2bool, isNull @@ -19,6 +22,36 @@ from .models import Build, BuildItem, BuildOrderAttachment from .serializers import BuildAttachmentSerializer, BuildSerializer, BuildItemSerializer +class BuildFilter(rest_filters.FilterSet): + """ + Custom filterset for BuildList API endpoint + """ + + status = rest_filters.NumberFilter(label='Status') + + active = rest_filters.BooleanFilter(label='Build is active', method='filter_active') + + def filter_active(self, queryset, name, value): + + if str2bool(value): + queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES) + else: + queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES) + + return queryset + + overdue = rest_filters.BooleanFilter(label='Build is overdue', method='filter_overdue') + + def filter_overdue(self, queryset, name, value): + + if str2bool(value): + queryset = queryset.filter(Build.OVERDUE_FILTER) + else: + queryset = queryset.exclude(Build.OVERDUE_FILTER) + + return queryset + + class BuildList(generics.ListCreateAPIView): """ API endpoint for accessing a list of Build objects. @@ -28,6 +61,7 @@ class BuildList(generics.ListCreateAPIView): queryset = Build.objects.all() serializer_class = BuildSerializer + filterset_class = BuildFilter filter_backends = [ DjangoFilterBackend, @@ -97,34 +131,6 @@ class BuildList(generics.ListCreateAPIView): except (ValueError, Build.DoesNotExist): pass - # Filter by build status? - status = params.get('status', None) - - if status is not None: - queryset = queryset.filter(status=status) - - # Filter by "pending" status - active = params.get('active', None) - - if active is not None: - active = str2bool(active) - - if active: - queryset = queryset.filter(status__in=BuildStatus.ACTIVE_CODES) - else: - queryset = queryset.exclude(status__in=BuildStatus.ACTIVE_CODES) - - # Filter by "overdue" status? - overdue = params.get('overdue', None) - - if overdue is not None: - overdue = str2bool(overdue) - - if overdue: - queryset = queryset.filter(Build.OVERDUE_FILTER) - else: - queryset = queryset.exclude(Build.OVERDUE_FILTER) - # Filter by associated part? part = params.get('part', None) From 9947a0cf905131b1ca7a21284f08a4baa0c45f3e Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 00:59:56 +1000 Subject: [PATCH 08/15] PEP fix --- InvenTree/build/api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index ee68dd0c60..069b6e58fe 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -6,7 +6,6 @@ JSON API for the Build app from __future__ import unicode_literals from django.conf.urls import url, include -from django.db.models import query from rest_framework import filters from rest_framework import generics From 60e8a17f0702e2945481bdbdc1de8ccb8fa255fd Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 01:06:38 +1000 Subject: [PATCH 09/15] bug fix: Prevent API forms from being submitted multiple times - A problem if you're a manic clicker --- InvenTree/templates/js/forms.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 4af3a6f45e..1de62dd3f5 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -511,6 +511,10 @@ function insertConfirmButton(options) { */ function submitFormData(fields, options) { + // Immediately disable the "submit" button, + // to prevent the form being submitted multiple times! + $(options.modal).find('#modal-form-submit').prop('disabled', true); + // Form data to be uploaded to the server // Only used if file / image upload is required var form_data = new FormData(); @@ -778,6 +782,9 @@ function clearFormErrors(options) { */ function handleFormErrors(errors, fields, options) { + // Reset the status of the "submit" button + $(options.modal).find('#modal-form-submit').prop('disabled', false); + // Remove any existing error messages from the form clearFormErrors(options); From ecf47aa69db44682ef54612b44b47ef76e605889 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 01:20:29 +1000 Subject: [PATCH 10/15] Fix for BuildOrder reference default value --- InvenTree/build/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 774e606040..5f8af9096b 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -53,7 +53,7 @@ def get_next_build_number(): reference = build.reference while 1: - reference = increment(build.reference) + reference = increment(reference) if reference in attempts: # Escape infinite recursion From 337223b8eb858601e26a53a0ec4908340fb6b11a Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 01:20:40 +1000 Subject: [PATCH 11/15] Modal form improvements --- InvenTree/templates/js/forms.js | 24 ++++++++++++++++++++++-- InvenTree/templates/js/modals.js | 2 -- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 1de62dd3f5..f764ea9062 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -732,11 +732,31 @@ function handleFormSuccess(response, options) { // Close the modal if (!options.preventClose) { - // TODO: Actually just *delete* the modal, - // rather than hiding it!! + // Note: The modal will be deleted automatically after closing $(options.modal).modal('hide'); } + // Display any required messages + // Should we show alerts immediately or cache them? + var cache = (options.follow && response.url) || options.redirect || options.reload; + + // Display any messages + if (response.success) { + showAlertOrCache("alert-success", response.success, cache); + } + + if (response.info) { + showAlertOrCache("alert-info", response.info, cache); + } + + if (response.warning) { + showAlertOrCache("alert-warning", response.warning, cache); + } + + if (response.danger) { + showAlertOrCache("alert-danger", response.danger, cache); + } + if (options.onSuccess) { // Callback function options.onSuccess(response, options); diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index d0f9f742f8..b613ed81f6 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -83,8 +83,6 @@ function createNewModal(options={}) { // Capture "enter" key input $(modal_name).on('keydown', 'input', function(event) { - - if (event.keyCode == 13) { event.preventDefault(); // Simulate a click on the 'Submit' button From 6e26bd0b71bd03786832a6962d574aa98fee5dc6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 01:29:17 +1000 Subject: [PATCH 12/15] Fixes for unit tests --- InvenTree/build/test_build.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/InvenTree/build/test_build.py b/InvenTree/build/test_build.py index a3b69646dd..b572feb14b 100644 --- a/InvenTree/build/test_build.py +++ b/InvenTree/build/test_build.py @@ -5,10 +5,11 @@ from django.test import TestCase from django.core.exceptions import ValidationError from django.db.utils import IntegrityError -from build.models import Build, BuildItem +from InvenTree import status_codes as status + +from build.models import Build, BuildItem, get_next_build_number from stock.models import StockItem from part.models import Part, BomItem -from InvenTree import status_codes as status class BuildTest(TestCase): @@ -80,8 +81,14 @@ class BuildTest(TestCase): quantity=2 ) + ref = get_next_build_number() + + if ref is None: + ref = "0001" + # Create a "Build" object to make 10x objects self.build = Build.objects.create( + reference=ref, title="This is a build", part=self.assembly, quantity=10 From cbd291849c99ece65ee0559bc93cf7f3d5eb8a35 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 01:49:17 +1000 Subject: [PATCH 13/15] More unit test fixes --- InvenTree/stock/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py index 6bc15b3505..205fb417a8 100644 --- a/InvenTree/stock/tests.py +++ b/InvenTree/stock/tests.py @@ -100,7 +100,7 @@ class StockTest(TestCase): # And there should be *no* items being build self.assertEqual(part.quantity_being_built, 0) - build = Build.objects.create(part=part, title='A test build', quantity=1) + build = Build.objects.create(reference='12345', part=part, title='A test build', quantity=1) # Add some stock items which are "building" for i in range(10): From cbf0e0bd4afafe3273bba58dfc121b7be58293eb Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 01:56:04 +1000 Subject: [PATCH 14/15] Refactor forms for editing and deleting a PartParameter - PartParameters now loaded using the API, not pre-rendered --- InvenTree/part/api.py | 6 +- InvenTree/part/models.py | 4 +- InvenTree/part/serializers.py | 29 ++--- .../part/templates/part/param_delete.html | 5 - InvenTree/part/templates/part/params.html | 40 ++----- InvenTree/part/test_api.py | 6 +- InvenTree/part/urls.py | 2 - InvenTree/part/views.py | 22 ---- .../templates/InvenTree/settings/part.html | 2 +- InvenTree/templates/js/forms.js | 8 +- InvenTree/templates/js/part.js | 101 ++++++++++++++++++ 11 files changed, 141 insertions(+), 84 deletions(-) delete mode 100644 InvenTree/part/templates/part/param_delete.html diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 7303f06787..2f66225a90 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1115,10 +1115,10 @@ part_api_urls = [ # Base URL for PartParameter API endpoints url(r'^parameter/', include([ - url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-param-template-list'), + url(r'^template/$', PartParameterTemplateList.as_view(), name='api-part-parameter-template-list'), - url(r'^(?P\d+)/', PartParameterDetail.as_view(), name='api-part-param-detail'), - url(r'^.*$', PartParameterList.as_view(), name='api-part-param-list'), + url(r'^(?P\d+)/', PartParameterDetail.as_view(), name='api-part-parameter-detail'), + url(r'^.*$', PartParameterList.as_view(), name='api-part-parameter-list'), ])), url(r'^thumbs/', include([ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index cc533177d9..8fe5744f06 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2164,7 +2164,7 @@ class PartParameterTemplate(models.Model): @staticmethod def get_api_url(): - return reverse('api-part-param-template-list') + return reverse('api-part-parameter-template-list') def __str__(self): s = str(self.name) @@ -2205,7 +2205,7 @@ class PartParameter(models.Model): @staticmethod def get_api_url(): - return reverse('api-part-param-list') + return reverse('api-part-parameter-list') def __str__(self): # String representation of a PartParameter (used in the admin interface) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 313e2cf920..6627639bca 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -508,19 +508,6 @@ class BomItemSerializer(InvenTreeModelSerializer): ] -class PartParameterSerializer(InvenTreeModelSerializer): - """ JSON serializers for the PartParameter model """ - - class Meta: - model = PartParameter - fields = [ - 'pk', - 'part', - 'template', - 'data' - ] - - class PartParameterTemplateSerializer(InvenTreeModelSerializer): """ JSON serializer for the PartParameterTemplate model """ @@ -533,6 +520,22 @@ class PartParameterTemplateSerializer(InvenTreeModelSerializer): ] +class PartParameterSerializer(InvenTreeModelSerializer): + """ JSON serializers for the PartParameter model """ + + template_detail = PartParameterTemplateSerializer(source='template', many=False, read_only=True) + + class Meta: + model = PartParameter + fields = [ + 'pk', + 'part', + 'template', + 'template_detail', + 'data' + ] + + class CategoryParameterTemplateSerializer(InvenTreeModelSerializer): """ Serializer for PartCategoryParameterTemplate """ diff --git a/InvenTree/part/templates/part/param_delete.html b/InvenTree/part/templates/part/param_delete.html deleted file mode 100644 index efb8ca3c26..0000000000 --- a/InvenTree/part/templates/part/param_delete.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "modal_delete_form.html" %} - -{% block pre_form_content %} -Are you sure you want to remove this parameter? -{% endblock %} \ No newline at end of file diff --git a/InvenTree/part/templates/part/params.html b/InvenTree/part/templates/part/params.html index e1c21cd681..8909d2bbbd 100644 --- a/InvenTree/part/templates/part/params.html +++ b/InvenTree/part/templates/part/params.html @@ -21,41 +21,23 @@ - - - - - - - - - - {% for param in part.get_parameters %} - - - - - - {% endfor %} - -
{% trans "Name" %}{% trans "Value" %}{% trans "Units" %}
{{ param.template.name }}{{ param.data }} - {{ param.template.units }} -
- {% if roles.part.change %} - - {% endif %} - {% if roles.part.change %} - - {% endif %} -
-
- +
{% endblock %} {% block js_ready %} {{ block.super }} + loadPartParameterTable( + '#parameter-table', + '{% url "api-part-parameter-list" %}', + { + params: { + part: {{ part.pk }}, + } + } + ); + $('#param-table').inventreeTable({ }); diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index 0c1f083383..7700c5c61f 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -805,7 +805,7 @@ class PartParameterTest(InvenTreeAPITestCase): Test for listing part parameters """ - url = reverse('api-part-param-list') + url = reverse('api-part-parameter-list') response = self.client.get(url, format='json') @@ -838,7 +838,7 @@ class PartParameterTest(InvenTreeAPITestCase): Test that we can create a param via the API """ - url = reverse('api-part-param-list') + url = reverse('api-part-parameter-list') response = self.client.post( url, @@ -860,7 +860,7 @@ class PartParameterTest(InvenTreeAPITestCase): Tests for the PartParameter detail endpoint """ - url = reverse('api-part-param-detail', kwargs={'pk': 5}) + url = reverse('api-part-parameter-detail', kwargs={'pk': 5}) response = self.client.get(url) diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 96560a7ad7..41880d9878 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -35,8 +35,6 @@ part_parameter_urls = [ url(r'^template/(?P\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'), - url(r'^(?P\d+)/edit/', views.PartParameterEdit.as_view(), name='part-param-edit'), - url(r'^(?P\d+)/delete/', views.PartParameterDelete.as_view(), name='part-param-delete'), ] part_detail_urls = [ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index d9f79262d1..360a47ac1e 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2307,28 +2307,6 @@ class PartParameterCreate(AjaxCreateView): return form -class PartParameterEdit(AjaxUpdateView): - """ View for editing a PartParameter """ - - model = PartParameter - form_class = part_forms.EditPartParameterForm - ajax_form_title = _('Edit Part Parameter') - - def get_form(self): - - form = super().get_form() - - return form - - -class PartParameterDelete(AjaxDeleteView): - """ View for deleting a PartParameter """ - - model = PartParameter - ajax_template_name = 'part/param_delete.html' - ajax_form_title = _('Delete Part Parameter') - - class CategoryDetail(InvenTreeRoleMixin, DetailView): """ Detail view for PartCategory """ diff --git a/InvenTree/templates/InvenTree/settings/part.html b/InvenTree/templates/InvenTree/settings/part.html index d4b386e77f..57ffc95ba8 100644 --- a/InvenTree/templates/InvenTree/settings/part.html +++ b/InvenTree/templates/InvenTree/settings/part.html @@ -75,7 +75,7 @@ {{ block.super }} $("#param-table").inventreeTable({ - url: "{% url 'api-part-param-template-list' %}", + url: "{% url 'api-part-parameter-template-list' %}", queryParams: { ordering: 'name', }, diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index f764ea9062..9c67622753 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -741,19 +741,19 @@ function handleFormSuccess(response, options) { var cache = (options.follow && response.url) || options.redirect || options.reload; // Display any messages - if (response.success) { + if (response && response.success) { showAlertOrCache("alert-success", response.success, cache); } - if (response.info) { + if (response && response.info) { showAlertOrCache("alert-info", response.info, cache); } - if (response.warning) { + if (response && response.warning) { showAlertOrCache("alert-warning", response.warning, cache); } - if (response.danger) { + if (response && response.danger) { showAlertOrCache("alert-danger", response.danger, cache); } diff --git a/InvenTree/templates/js/part.js b/InvenTree/templates/js/part.js index 7fa63098e1..e106098ad4 100644 --- a/InvenTree/templates/js/part.js +++ b/InvenTree/templates/js/part.js @@ -220,6 +220,107 @@ function loadSimplePartTable(table, url, options={}) { } +function loadPartParameterTable(table, url, options) { + + var params = options.params || {}; + + // Load filters + var filters = loadTableFilters("part-parameters"); + + for (var key in params) { + filters[key] = params[key]; + } + + // setupFilterLsit("#part-parameters", $(table)); + + $(table).inventreeTable({ + url: url, + original: params, + queryParams: filters, + name: 'partparameters', + groupBy: false, + formatNoMatches: function() { return '{% trans "No parameters found" %}'; }, + columns: [ + { + checkbox: true, + switchable: false, + visible: true, + }, + { + field: 'name', + title: '{% trans "Name" %}', + switchable: false, + sortable: true, + formatter: function(value, row) { + return row.template_detail.name; + } + }, + { + field: 'data', + title: '{% trans "Value" %}', + switchable: false, + sortable: true, + }, + { + field: 'units', + title: '{% trans "Units" %}', + switchable: true, + sortable: true, + formatter: function(value, row) { + return row.template_detail.units; + } + }, + { + field: 'actions', + title: '', + switchable: false, + sortable: false, + formatter: function(value, row) { + var pk = row.pk; + + var html = `
`; + + html += makeIconButton('fa-edit icon-blue', 'button-parameter-edit', pk, '{% trans "Edit parameter" %}'); + html += makeIconButton('fa-trash-alt icon-red', 'button-parameter-delete', pk, '{% trans "Delete parameter" %}'); + + html += `
`; + + return html; + } + } + ], + onPostBody: function() { + // Setup button callbacks + $(table).find('.button-parameter-edit').click(function() { + var pk = $(this).attr('pk'); + + constructForm(`/api/part/parameter/${pk}/`, { + fields: { + data: {}, + }, + title: '{% trans "Edit Parameter" %}', + onSuccess: function() { + $(table).bootstrapTable('refresh'); + } + }); + }); + + $(table).find('.button-parameter-delete').click(function() { + var pk = $(this).attr('pk'); + + constructForm(`/api/part/parameter/${pk}/`, { + method: 'DELETE', + title: '{% trans "Delete Parameter" %}', + onSuccess: function() { + $(table).bootstrapTable('refresh'); + } + }); + }); + } + }); +} + + function loadParametricPartTable(table, options={}) { /* Load parametric table for part parameters * From 0a86d947bc1b9efffda851f9c82d6d9be9aebebb Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 9 Jul 2021 02:03:33 +1000 Subject: [PATCH 15/15] Refactor PartParameterCreate form --- InvenTree/part/templates/part/params.html | 23 ++++++---- InvenTree/part/urls.py | 2 - InvenTree/part/views.py | 52 +---------------------- InvenTree/templates/js/forms.js | 3 ++ InvenTree/templates/js/model_renderers.js | 8 ++++ 5 files changed, 27 insertions(+), 61 deletions(-) diff --git a/InvenTree/part/templates/part/params.html b/InvenTree/part/templates/part/params.html index 8909d2bbbd..365003b052 100644 --- a/InvenTree/part/templates/part/params.html +++ b/InvenTree/part/templates/part/params.html @@ -43,14 +43,21 @@ {% if roles.part.add %} $('#param-create').click(function() { - launchModalForm("{% url 'part-param-create' %}?part={{ part.id }}", { - reload: true, - secondary: [{ - field: 'template', - label: '{% trans "New Template" %}', - title: '{% trans "Create New Parameter Template" %}', - url: "{% url 'part-param-template-create' %}" - }], + + constructForm('{% url "api-part-parameter-list" %}', { + method: 'POST', + fields: { + part: { + value: {{ part.pk }}, + hidden: true, + }, + template: {}, + data: {}, + }, + title: '{% trans "Add Parameter" %}', + onSuccess: function() { + $('#parameter-table').bootstrapTable('refresh'); + } }); }); {% endif %} diff --git a/InvenTree/part/urls.py b/InvenTree/part/urls.py index 41880d9878..6bd8d02601 100644 --- a/InvenTree/part/urls.py +++ b/InvenTree/part/urls.py @@ -33,8 +33,6 @@ part_parameter_urls = [ url(r'^template/new/', views.PartParameterTemplateCreate.as_view(), name='part-param-template-create'), url(r'^template/(?P\d+)/edit/', views.PartParameterTemplateEdit.as_view(), name='part-param-template-edit'), url(r'^template/(?P\d+)/delete/', views.PartParameterTemplateDelete.as_view(), name='part-param-template-edit'), - - url(r'^new/', views.PartParameterCreate.as_view(), name='part-param-create'), ] part_detail_urls = [ diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 360a47ac1e..d349557816 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -32,7 +32,7 @@ from rapidfuzz import fuzz from decimal import Decimal, InvalidOperation from .models import PartCategory, Part, PartRelated -from .models import PartParameterTemplate, PartParameter +from .models import PartParameterTemplate from .models import PartCategoryParameterTemplate from .models import BomItem from .models import match_part_names @@ -2257,56 +2257,6 @@ class PartParameterTemplateDelete(AjaxDeleteView): ajax_form_title = _("Delete Part Parameter Template") -class PartParameterCreate(AjaxCreateView): - """ View for creating a new PartParameter """ - - model = PartParameter - form_class = part_forms.EditPartParameterForm - ajax_form_title = _('Create Part Parameter') - - def get_initial(self): - - initials = {} - - part_id = self.request.GET.get('part', None) - - if part_id: - try: - initials['part'] = Part.objects.get(pk=part_id) - except (Part.DoesNotExist, ValueError): - pass - - return initials - - def get_form(self): - """ Return the form object. - - - Hide the 'Part' field (specified in URL) - - Limit the 'Template' options (to avoid duplicates) - """ - - form = super().get_form() - - part_id = self.request.GET.get('part', None) - - if part_id: - try: - part = Part.objects.get(pk=part_id) - - form.fields['part'].widget = HiddenInput() - - query = form.fields['template'].queryset - - query = query.exclude(id__in=[param.template.id for param in part.parameters.all()]) - - form.fields['template'].queryset = query - - except (Part.DoesNotExist, ValueError): - pass - - return form - - class CategoryDetail(InvenTreeRoleMixin, DetailView): """ Detail view for PartCategory """ diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 9c67622753..b7af665393 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -1228,6 +1228,9 @@ function renderModelData(name, model, data, parameters, options) { case 'partcategory': renderer = renderPartCategory; break; + case 'partparametertemplate': + renderer = renderPartParameterTemplate; + break; case 'supplierpart': renderer = renderSupplierPart; break; diff --git a/InvenTree/templates/js/model_renderers.js b/InvenTree/templates/js/model_renderers.js index 91736e0b5a..9e98199bfa 100644 --- a/InvenTree/templates/js/model_renderers.js +++ b/InvenTree/templates/js/model_renderers.js @@ -166,6 +166,14 @@ function renderPartCategory(name, data, parameters, options) { } +function renderPartParameterTemplate(name, data, parameters, options) { + + var html = `${data.name} - [${data.units}]`; + + return html; +} + + // Rendered for "SupplierPart" model function renderSupplierPart(name, data, parameters, options) {