From 4eb8c60ee02338c7bbf95c956093f41f7d1de734 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 12 Oct 2021 22:22:49 +1100 Subject: [PATCH 01/22] 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 a00dc9b0b1779ee8218917bca4c75823081b7854 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 10:50:24 +1100 Subject: [PATCH 02/22] Add migration file for new database model --- .../part/migrations/0072_bomitemsubstitute.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 InvenTree/part/migrations/0072_bomitemsubstitute.py diff --git a/InvenTree/part/migrations/0072_bomitemsubstitute.py b/InvenTree/part/migrations/0072_bomitemsubstitute.py new file mode 100644 index 0000000000..2e48b10a8d --- /dev/null +++ b/InvenTree/part/migrations/0072_bomitemsubstitute.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.5 on 2021-10-12 23:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0071_alter_partparametertemplate_name'), + ] + + operations = [ + migrations.CreateModel( + name='BomItemSubstitute', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bom_item', models.ForeignKey(help_text='Parent BOM item', on_delete=django.db.models.deletion.CASCADE, related_name='substitutes', to='part.bomitem', verbose_name='BOM Item')), + ('part', models.ForeignKey(help_text='Substitute part', limit_choices_to={'component': True}, on_delete=django.db.models.deletion.CASCADE, related_name='substitute_items', to='part.part', verbose_name='Part')), + ], + ), + ] From 7c396f9f39573e719746552ba2ebf70cfe081218 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 11:07:34 +1100 Subject: [PATCH 03/22] Enable optional pagination for the BomItem API endpoint --- InvenTree/part/api.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index de37d4ea52..0c8c686503 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1078,11 +1078,23 @@ class BomList(generics.ListCreateAPIView): queryset = self.filter_queryset(self.get_queryset()) - serializer = self.get_serializer(queryset, many=True) + page = self.paginate_queryset(queryset) + + if page is not None: + serializer = self.get_serializer(page, many=True) + else: + serializer = self.get_serializer(queryset, many=True) data = serializer.data - if request.is_ajax(): + """ + Determine the response type based on the request. + a) For HTTP requests (e.g. via the browseable API) return a DRF response + b) For AJAX requests, simply return a JSON rendered response. + """ + if page is not None: + return self.get_paginated_response(data) + elif request.is_ajax(): return JsonResponse(data, safe=False) else: return Response(data) @@ -1147,7 +1159,9 @@ class BomList(generics.ListCreateAPIView): except (ValueError, Part.DoesNotExist): pass - include_pricing = str2bool(params.get('include_pricing', True)) + pricing_default = InvenTreeSetting.get_setting('PART_SHOW_PRICE_IN_BOM') + + include_pricing = str2bool(params.get('include_pricing', pricing_default)) if include_pricing: queryset = self.annotate_pricing(queryset) From 324ccd805dad006afcafa20f57545e534944b36d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 11:07:44 +1100 Subject: [PATCH 04/22] Include BomItem substitutes in the serializer --- InvenTree/part/serializers.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 509de43b68..3fc8e30c1e 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -389,7 +389,9 @@ class PartStarSerializer(InvenTreeModelSerializer): class BomItemSerializer(InvenTreeModelSerializer): - """ Serializer for BomItem object """ + """ + Serializer for BomItem object + """ price_range = serializers.CharField(read_only=True) @@ -397,6 +399,8 @@ class BomItemSerializer(InvenTreeModelSerializer): part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True)) + substitutes = PartBriefSerializer(many=True, read_only=True) + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) sub_part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(component=True)) @@ -515,6 +519,7 @@ class BomItemSerializer(InvenTreeModelSerializer): 'reference', 'sub_part', 'sub_part_detail', + 'substitutes', 'price_range', 'validated', ] From f3f41730be32765dc4037df3fd9e72f0d3822b7d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 11:58:40 +1100 Subject: [PATCH 05/22] Add "substitutes" column to BOM table --- InvenTree/templates/js/translated/bom.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 3524bc5c40..51866732ba 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -300,6 +300,20 @@ function loadBomTable(table, options) { return renderLink(text, url); } }); + + cols.push({ + field: 'substitutes', + title: '{% trans "Substitutes" %}', + searchable: false, + sortable: true, + formatter: function(value, row) { + if (row.substitutes) { + return row.substitutes.length; + } else { + return '-'; + } + } + }); if (show_pricing) { cols.push({ From 37bd5c05073941a22c820f0658f33143e8d20c33 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 13:58:41 +1100 Subject: [PATCH 06/22] Add serializer and API endpoint for BomItemSubstitute class --- InvenTree/part/api.py | 59 ++++++++++++++++++++++++++++++----- InvenTree/part/models.py | 4 +++ InvenTree/part/serializers.py | 22 +++++++++++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 0c8c686503..834ac93eed 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -27,7 +27,8 @@ from djmoney.contrib.exchange.exceptions import MissingRate from decimal import Decimal, InvalidOperation -from .models import Part, PartCategory, BomItem +from .models import Part, PartCategory +from .models import BomItem, BomItemSubstitute from .models import PartParameter, PartParameterTemplate from .models import PartAttachment, PartTestTemplate from .models import PartSellPriceBreak, PartInternalPriceBreak @@ -1114,7 +1115,7 @@ class BomList(generics.ListCreateAPIView): try: # Include or exclude pricing information in the serialized data - kwargs['include_pricing'] = str2bool(self.request.GET.get('include_pricing', True)) + kwargs['include_pricing'] = self.include_pricing() except AttributeError: pass @@ -1159,15 +1160,19 @@ class BomList(generics.ListCreateAPIView): except (ValueError, Part.DoesNotExist): pass - pricing_default = InvenTreeSetting.get_setting('PART_SHOW_PRICE_IN_BOM') - - include_pricing = str2bool(params.get('include_pricing', pricing_default)) - - if include_pricing: + if self.include_pricing(): queryset = self.annotate_pricing(queryset) return queryset + def include_pricing(self): + """ + Determine if pricing information should be included in the response + """ + pricing_default = InvenTreeSetting.get_setting('PART_SHOW_PRICE_IN_BOM') + + return str2bool(self.request.query_params.get('include_pricing', pricing_default)) + def annotate_pricing(self, queryset): """ Add part pricing information to the queryset @@ -1276,6 +1281,36 @@ class BomItemValidate(generics.UpdateAPIView): return Response(serializer.data) + +class BomItemSubstituteList(generics.ListCreateAPIView): + """ + API endpoint for accessing a list of BomItemSubstitute objects + """ + + serializer_class = part_serializers.BomItemSubstituteSerializer + queryset = BomItemSubstitute.objects.all() + + filter_backends = [ + DjangoFilterBackend, + filters.SearchFilter, + filters.OrderingFilter, + ] + + filter_fields = [ + 'part', + 'bom_item', + ] + + +class BomItemSubstituteDetail(generics.RetrieveUpdateDestroyAPIView): + """ + API endpoint for detail view of a single BomItemSubstitute object + """ + + queryset = BomItemSubstitute.objects.all() + serializer_class = part_serializers.BomItemSubstituteSerializer + + part_api_urls = [ url(r'^tree/?', PartCategoryTree.as_view(), name='api-part-tree'), @@ -1328,6 +1363,16 @@ part_api_urls = [ ] bom_api_urls = [ + + url(r'^substitute/', include([ + + # Detail view + url(r'^(?P\d+)/', BomItemSubstituteDetail.as_view(), name='api-bom-substitute-detail'), + + # Catch all + url(r'^.*$', BomItemSubstituteList.as_view(), name='api-bom-substitute-list'), + ])), + # BOM Item Detail url(r'^(?P\d+)/', include([ url(r'^validate/?', BomItemValidate.as_view(), name='api-bom-item-validate'), diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index f883250811..0be1f5fc31 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2623,6 +2623,10 @@ class BomItemSubstitute(models.Model): part: The part which can be used as a substitute """ + @staticmethod + def get_api_url(): + return reverse('api-bom-substitute-list') + bom_item = models.ForeignKey( BomItem, on_delete=models.CASCADE, diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 3fc8e30c1e..869145e1af 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -23,7 +23,8 @@ from InvenTree.serializers import (InvenTreeAttachmentSerializerField, from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus from stock.models import StockItem -from .models import (BomItem, Part, PartAttachment, PartCategory, +from .models import (BomItem, BomItemSubstitute, + Part, PartAttachment, PartCategory, PartParameter, PartParameterTemplate, PartSellPriceBreak, PartStar, PartTestTemplate, PartCategoryParameterTemplate, PartInternalPriceBreak) @@ -388,6 +389,23 @@ class PartStarSerializer(InvenTreeModelSerializer): ] +class BomItemSubstituteSerializer(InvenTreeModelSerializer): + """ + Serializer for the BomItemSubstitute class + """ + + part_detail = PartBriefSerializer(source='part', read_only=True, many=False) + + class Meta: + model = BomItemSubstitute + fields = [ + 'pk', + 'bom_item', + 'part', + 'part_detail', + ] + + class BomItemSerializer(InvenTreeModelSerializer): """ Serializer for BomItem object @@ -399,7 +417,7 @@ class BomItemSerializer(InvenTreeModelSerializer): part = serializers.PrimaryKeyRelatedField(queryset=Part.objects.filter(assembly=True)) - substitutes = PartBriefSerializer(many=True, read_only=True) + substitutes = BomItemSubstituteSerializer(many=True, read_only=True) part_detail = PartBriefSerializer(source='part', many=False, read_only=True) From 51efd6b2e4d6f1e7304e312ddd14d422262dc91d Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 14:06:36 +1100 Subject: [PATCH 07/22] Add permissions for new mdoel --- InvenTree/users/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py index 73a7561153..d31f2a9905 100644 --- a/InvenTree/users/models.py +++ b/InvenTree/users/models.py @@ -81,6 +81,7 @@ class RuleSet(models.Model): 'part': [ 'part_part', 'part_bomitem', + 'part_bomitemsubstitute', 'part_partattachment', 'part_partsellpricebreak', 'part_partinternalpricebreak', @@ -110,6 +111,7 @@ class RuleSet(models.Model): 'part_part', 'part_partcategory', 'part_bomitem', + 'part_bomitemsubstitute', 'build_build', 'build_builditem', 'build_buildorderattachment', From 6816071388e87e88b82728a0cb16e15683002fe8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 14:44:59 +1100 Subject: [PATCH 08/22] Simplify BOM editing view - A user with permission to edit BOM data can immediately access the BOM editing tools --- InvenTree/part/templates/part/bom.html | 15 +++------------ InvenTree/part/templates/part/detail.html | 22 ++++++---------------- InvenTree/part/test_views.py | 10 ---------- InvenTree/part/views.py | 11 ++--------- 4 files changed, 11 insertions(+), 47 deletions(-) diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 750e205e04..6808630ed1 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -1,7 +1,7 @@ {% load i18n %} {% load inventree_extras %} -{% if roles.part.change != True and editing_enabled %} +{% if not roles.part.change %}
{% trans "You do not have permission to edit the BOM." %}
@@ -23,7 +23,7 @@
- {% if editing_enabled %} + {% if roles.part.change %} @@ -35,17 +35,9 @@ {% endif %} - - - {% elif part.active %} - {% if roles.part.change %} - {% if part.is_bom_valid == False %} `; + var bValidate = makeIconButton('fa-check-circle icon-green', 'bom-validate-button', row.pk, '{% trans "Validate BOM Item" %}'); var bValid = ``; - var bEdit = ``; + var bSubs = makeIconButton('fa-exchange-alt icon-blue', 'bom-substitutes-button', row.pk, '{% trans "Edit substitute parts" %}'); - var bDelt = ``; + var bEdit = makeIconButton('fa-edit icon-blue', 'bom-edit-button', row.pk, '{% trans "Edit BOM Item" %}'); - var html = `
`; + var bDelt = makeIconButton('fa-trash-alt icon-red', 'bom-delete-button', row.pk, '{% trans "Delete BOM Item" %}'); - html += bEdit; - html += bDelt; + var html = `
`; if (!row.validated) { html += bValidate; @@ -457,6 +543,10 @@ function loadBomTable(table, options) { html += bValid; } + html += bEdit; + html += bSubs; + html += bDelt; + html += `
`; return html; @@ -508,6 +598,7 @@ function loadBomTable(table, options) { treeEnable: !options.editable, rootParentId: parent_id, idField: 'pk', + uniqueId: 'pk', parentIdField: 'parentId', treeShowField: 'sub_part', showColumns: true, @@ -584,19 +675,27 @@ function loadBomTable(table, options) { // In editing mode, attached editables to the appropriate table elements if (options.editable) { + // Callback for "delete" button table.on('click', '.bom-delete-button', function() { var pk = $(this).attr('pk'); + var html = ` +
+ {% trans "Are you sure you want to delete this BOM item?" %} +
`; + constructForm(`/api/bom/${pk}/`, { method: 'DELETE', title: '{% trans "Delete BOM Item" %}', + preFormContent: html, onSuccess: function() { reloadBomTable(table); } }); }); + // Callback for "edit" button table.on('click', '.bom-edit-button', function() { var pk = $(this).attr('pk'); @@ -613,6 +712,7 @@ function loadBomTable(table, options) { }); }); + // Callback for "validate" button table.on('click', '.bom-validate-button', function() { var pk = $(this).attr('pk'); @@ -631,5 +731,21 @@ function loadBomTable(table, options) { } ); }); + + // Callback for "substitutes" button + table.on('click', '.bom-substitutes-button', function() { + var pk = $(this).attr('pk'); + + var row = table.bootstrapTable('getRowByUniqueId', pk); + var subs = row.substitutes || []; + + bomSubstitutesDialog( + pk, + subs, + { + + } + ); + }); } } diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index 33eef49e48..cf675ff4f7 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -159,21 +159,25 @@ function renderPart(name, data, parameters, options) { html += ` - ${data.description}`; } - var stock = ''; + var extra = ''; // Display available part quantity if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) { if (data.in_stock == 0) { - stock = `{% trans "No Stock" %}`; + extra += `{% trans "No Stock" %}`; } else { - stock = `{% trans "In Stock" %}: ${data.in_stock}`; + extra += `{% trans "Stock" %}: ${data.in_stock}`; } } + if (!data.active) { + extra += `{% trans "Inactive" %}`; + } + html += ` - ${stock} + ${extra} {% trans "Part ID" %}: ${data.pk} `; From 0e1e8226b16fe14f09d32d82973ce1cbba4898a5 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 21:42:50 +1100 Subject: [PATCH 11/22] Add a callback to remove individual rows --- InvenTree/templates/js/translated/bom.js | 25 ++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 4f3ae912d5..a5e1153bad 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -162,7 +162,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { // Render a single row var html = ` - + ${thumb} ${substitute.part_detail.full_name} ${substitute.part_detail.description} ${buttons} @@ -218,9 +218,30 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { }, }, preFormContent: html, + submitText: '{% trans "Add Substitute" %}', title: '{% trans "Edit BOM Item Substitutes" %}', afterRender: function(fields, opts) { - // TODO + + // Add a callback to remove individual rows + $(opts.modal).find('.button-row-remove').click(function() { + var pk = $(this).attr('pk'); + + var pre = ` +
+ {% trans "Are you sure you wish to remove this substitute part link?" %} +
+ `; + + constructForm(`/api/bom/substitute/${pk}/`, { + method: 'DELETE', + title: '{% trans "Remove Substitute Part" %}', + preFormContent: pre, + confirm: true, + onSuccess: function() { + $(opts.modal).find(`#substitute-row-${pk}`).remove(); + } + }); + }); }, onSubmit: function(fields, opts) { // TODO From ea4c4c514f6867c05494243f9e0557015ad43e22 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 22:01:32 +1100 Subject: [PATCH 12/22] Add uniqueness checking for the BomItemSubstitute model --- .../migrations/0073_auto_20211013_1048.py | 21 +++++++++++++++++ InvenTree/part/models.py | 20 ++++++++++++++++ InvenTree/templates/js/translated/bom.js | 23 +++++++++++++++++-- 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 InvenTree/part/migrations/0073_auto_20211013_1048.py diff --git a/InvenTree/part/migrations/0073_auto_20211013_1048.py b/InvenTree/part/migrations/0073_auto_20211013_1048.py new file mode 100644 index 0000000000..e581af603e --- /dev/null +++ b/InvenTree/part/migrations/0073_auto_20211013_1048.py @@ -0,0 +1,21 @@ +# Generated by Django 3.2.5 on 2021-10-13 10:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('part', '0072_bomitemsubstitute'), + ] + + operations = [ + migrations.AlterModelOptions( + name='bomitemsubstitute', + options={'verbose_name': 'BOM Item Substitute'}, + ), + migrations.AlterUniqueTogether( + name='bomitemsubstitute', + unique_together={('part', 'bom_item')}, + ), + ] diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 0be1f5fc31..85dd9042fc 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2623,6 +2623,26 @@ class BomItemSubstitute(models.Model): part: The part which can be used as a substitute """ + class Meta: + verbose_name = _("BOM Item Substitute") + + # Prevent duplication of substitute parts + unique_together = ('part', 'bom_item') + + def validate_unique(self, exclude=None): + """ + Ensure that this BomItemSubstitute is "unique": + + - It cannot point to the same "part" as the "sub_part" of the parent "bom_item" + """ + + super().validate_unique(exclude=exclude) + + if self.part == self.bom_item.sub_part: + raise ValidationError({ + "part": _("Substitute part cannot be the same as the master part"), + }) + @staticmethod def get_api_url(): return reverse('api-bom-substitute-list') diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index a5e1153bad..19426ba1d2 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -213,11 +213,16 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { constructForm('{% url "api-bom-substitute-list" %}', { method: 'POST', fields: { + bom_item: { + hidden: true, + value: bom_item_id, + }, part: { required: false, }, }, preFormContent: html, + cancelText: '{% trans "Close" %}', submitText: '{% trans "Add Substitute" %}', title: '{% trans "Edit BOM Item Substitutes" %}', afterRender: function(fields, opts) { @@ -243,8 +248,22 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { }); }); }, - onSubmit: function(fields, opts) { - // TODO + preventClose: true, + onSuccess: function(response, opts) { + + // Clear the form + var field = { + type: 'related field', + }; + + updateFieldValue('part', null, field, opts); + + // Add the new substitute to the table + var row = renderSubstituteRow(response); + $(opts.modal).find('#substitute-table > tbody:last-child').append(row); + + // Re-enable the "submit" button + $(opts.modal).find('#modal-form-submit').prop('disabled', false); } }); From 8512c2db757c101e7d6dff6063405d3ce6221478 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 22:06:51 +1100 Subject: [PATCH 13/22] Reload the parent table when adding or removing substitutes --- InvenTree/templates/js/translated/bom.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 19426ba1d2..c0dbdd4453 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -150,6 +150,13 @@ function newPartFromBomWizard(e) { */ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { + // Reload data for the parent table + function reloadParentTable() { + if (options.table) { + options.table.bootstrapTable('refresh'); + } + } + function renderSubstituteRow(substitute) { var pk = substitute.pk; @@ -244,6 +251,8 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { confirm: true, onSuccess: function() { $(opts.modal).find(`#substitute-row-${pk}`).remove(); + + reloadParentTable(); } }); }); @@ -264,6 +273,9 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { // Re-enable the "submit" button $(opts.modal).find('#modal-form-submit').prop('disabled', false); + + // Reload the parent BOM table + reloadParentTable(); } }); @@ -783,7 +795,7 @@ function loadBomTable(table, options) { pk, subs, { - + table: table, } ); }); From 0f8c279aa2bbc754b0ab52c0b7f68693215a623c Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 22:27:10 +1100 Subject: [PATCH 14/22] Add some unit testing for the new model --- InvenTree/part/api.py | 1 - InvenTree/part/models.py | 6 ++ InvenTree/part/test_bom_item.py | 71 +++++++++++++++++++++++- InvenTree/templates/js/translated/bom.js | 12 +++- 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 834ac93eed..dccc2f9ac1 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -1281,7 +1281,6 @@ class BomItemValidate(generics.UpdateAPIView): return Response(serializer.data) - class BomItemSubstituteList(generics.ListCreateAPIView): """ API endpoint for accessing a list of BomItemSubstitute objects diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 85dd9042fc..75a8249f7b 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2629,6 +2629,12 @@ class BomItemSubstitute(models.Model): # Prevent duplication of substitute parts unique_together = ('part', 'bom_item') + def save(self, *args, **kwargs): + + self.full_clean() + + super().save(*args, **kwargs) + def validate_unique(self, exclude=None): """ Ensure that this BomItemSubstitute is "unique": diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py index be9740d128..cd3c77845c 100644 --- a/InvenTree/part/test_bom_item.py +++ b/InvenTree/part/test_bom_item.py @@ -1,8 +1,13 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals +from django.db import transaction + from django.test import TestCase import django.core.exceptions as django_exceptions from decimal import Decimal -from .models import Part, BomItem +from .models import Part, BomItem, BomItemSubstitute class BomItemTest(TestCase): @@ -130,3 +135,67 @@ class BomItemTest(TestCase): self.bob.get_bom_price_range(1, internal=True), (Decimal(27.5), Decimal(87.5)) ) + + def test_substitutes(self): + """ + Tests for BOM item substitutes + """ + + # We will make some subtitute parts for the "orphan" part + bom_item = BomItem.objects.get( + part=self.bob, + sub_part=self.orphan + ) + + # No substitute parts available + self.assertEqual(bom_item.substitutes.count(), 0) + + subs = [] + + for ii in range(5): + + # Create a new part + sub_part = Part.objects.create( + name=f"Orphan {ii}", + description="A substitute part for the orphan part", + component=True, + is_template=False, + assembly=False, + ) + + subs.append(sub_part) + + # Link it as a substitute part + BomItemSubstitute.objects.create( + bom_item=bom_item, + part=sub_part + ) + + # Try to link it again (this should fail as it is a duplicate substitute) + with self.assertRaises(django_exceptions.ValidationError): + with transaction.atomic(): + BomItemSubstitute.objects.create( + bom_item=bom_item, + part=sub_part + ) + + # There should be now 5 substitute parts available + self.assertEqual(bom_item.substitutes.count(), 5) + + # Try to create a substitute which points to the same sub-part (should fail) + with self.assertRaises(django_exceptions.ValidationError): + BomItemSubstitute.objects.create( + bom_item=bom_item, + part=self.orphan, + ) + + # Remove one substitute part + bom_item.substitutes.last().delete() + + self.assertEqual(bom_item.substitutes.count(), 4) + + for sub in subs: + sub.delete() + + # The substitution links should have been automatically removed + self.assertEqual(bom_item.substitutes.count(), 0) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index c0dbdd4453..9d3380c653 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -161,7 +161,9 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { var pk = substitute.pk; - var thumb = thumbnailImage(substitute.part_detail.thumbnail || substitute.part_detail.image); + var part = substitute.part_detail; + + var thumb = thumbnailImage(part.thumbnail || part.image); var buttons = ''; @@ -170,8 +172,12 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) { // Render a single row var html = ` - ${thumb} ${substitute.part_detail.full_name} - ${substitute.part_detail.description} + + + ${thumb} ${part.full_name} + + + ${part.description} ${buttons} `; From f3074e8f3412bbfe11e3e7693a12efdecfe1db59 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 23:18:26 +1100 Subject: [PATCH 15/22] Improved unit testing for BomItem - tests for allowing variant parts - tests for allowing substitutes --- InvenTree/part/models.py | 23 ++- InvenTree/part/test_api.py | 294 +++++++++++++++++++++++++++++++------ 2 files changed, 263 insertions(+), 54 deletions(-) diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 75a8249f7b..7d0c87201c 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2337,18 +2337,29 @@ class BomItem(models.Model): """ Return a queryset filter for selecting StockItems which match this BomItem + - Allow stock from all directly specified substitute parts - If allow_variants is True, allow all part variants """ - # Target part - part = self.sub_part + # List of parts we allow + part_ids = set() + part_ids.add(self.sub_part.pk) + + # Variant parts (if allowed) if self.allow_variants: - variants = part.get_descendants(include_self=True) - return Q(part__in=[v.pk for v in variants]) - else: - return Q(part=part) + variants = self.sub_part.get_descendants(include_self=False) + + for v in variants: + part_ids.add(v.pk) + + # Direct substitute parts + for sub in self.substitutes.all(): + part_ids.add(sub.part.pk) + + # Return a list of Part ID values which can be filtered against + return Q(part__in=[pk for pk in part_ids]) def save(self, *args, **kwargs): diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index ac9d6bdf45..d6c1ba3741 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals import PIL @@ -11,7 +12,8 @@ from InvenTree.api_tester import InvenTreeAPITestCase from InvenTree.status_codes import StockStatus from part.models import Part, PartCategory -from stock.models import StockItem +from part.models import BomItem, BomItemSubstitute +from stock.models import StockItem, StockLocation from company.models import Company from common.models import InvenTreeSetting @@ -273,53 +275,6 @@ class PartAPITest(InvenTreeAPITestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 3) - def test_get_bom_list(self): - """ There should be 4 BomItem objects in the database """ - url = reverse('api-bom-list') - response = self.client.get(url, format='json') - self.assertEqual(len(response.data), 5) - - def test_get_bom_detail(self): - # Get the detail for a single BomItem - url = reverse('api-bom-item-detail', kwargs={'pk': 3}) - response = self.client.get(url, format='json') - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(int(float(response.data['quantity'])), 25) - - # Increase the quantity - data = response.data - data['quantity'] = 57 - data['note'] = 'Added a note' - - response = self.client.patch(url, data, format='json') - - # Check that the quantity was increased and a note added - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(int(float(response.data['quantity'])), 57) - self.assertEqual(response.data['note'], 'Added a note') - - def test_add_bom_item(self): - url = reverse('api-bom-list') - - data = { - 'part': 100, - 'sub_part': 4, - 'quantity': 777, - } - - response = self.client.post(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - # Now try to create a BomItem which points to a non-assembly part (should fail) - data['part'] = 3 - response = self.client.post(url, data, format='json') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - # TODO - Now try to create a BomItem which references itself - data['part'] = 2 - data['sub_part'] = 2 - response = self.client.post(url, data, format='json') - def test_test_templates(self): url = reverse('api-part-test-template-list') @@ -926,6 +881,249 @@ class PartAPIAggregationTest(InvenTreeAPITestCase): self.assertEqual(data['stock_item_count'], 105) +class BomItemTest(InvenTreeAPITestCase): + """ + Unit tests for the BomItem API + """ + + fixtures = [ + 'category', + 'part', + 'location', + 'stock', + 'bom', + 'company', + ] + + roles = [ + 'part.add', + 'part.change', + 'part.delete', + ] + + def setUp(self): + super().setUp() + + def test_bom_list(self): + """ + Tests for the BomItem list endpoint + """ + + # How many BOM items currently exist in the database? + n = BomItem.objects.count() + + url = reverse('api-bom-list') + response = self.get(url, expected_code=200) + self.assertEqual(len(response.data), n) + + # Now, filter by part + response = self.get( + url, + data={ + 'part': 100, + }, + expected_code=200 + ) + + print("results:", len(response.data)) + + def test_get_bom_detail(self): + """ + Get the detail view for a single BomItem object + """ + + url = reverse('api-bom-item-detail', kwargs={'pk': 3}) + + response = self.get(url, expected_code=200) + + self.assertEqual(int(float(response.data['quantity'])), 25) + + # Increase the quantity + data = response.data + data['quantity'] = 57 + data['note'] = 'Added a note' + + response = self.patch(url, data, expected_code=200) + + self.assertEqual(int(float(response.data['quantity'])), 57) + self.assertEqual(response.data['note'], 'Added a note') + + def test_add_bom_item(self): + """ + Test that we can create a new BomItem via the API + """ + + url = reverse('api-bom-list') + + data = { + 'part': 100, + 'sub_part': 4, + 'quantity': 777, + } + + self.post(url, data, expected_code=201) + + # Now try to create a BomItem which references itself + data['part'] = 100 + data['sub_part'] = 100 + self.client.post(url, data, expected_code=400) + + def test_variants(self): + """ + Tests for BomItem use with variants + """ + + stock_url = reverse('api-stock-list') + + # BOM item we are interested in + bom_item = BomItem.objects.get(pk=1) + + bom_item.allow_variants = True + bom_item.save() + + # sub part that the BOM item points to + sub_part = bom_item.sub_part + + sub_part.is_template = True + sub_part.save() + + # How many stock items are initially available for this part? + response = self.get( + stock_url, + { + 'bom_item': bom_item.pk, + }, + expected_code=200 + ) + + n_items = len(response.data) + self.assertEqual(n_items, 2) + + loc = StockLocation.objects.get(pk=1) + + # Now we will create some variant parts and stock + for ii in range(5): + + # Create a variant part! + variant = Part.objects.create( + name=f"Variant_{ii}", + description="A variant part", + component=True, + variant_of=sub_part + ) + + variant.save() + + Part.objects.rebuild() + + # Create some stock items for this new part + for jj in range(ii): + StockItem.objects.create( + part=variant, + location=loc, + quantity=100 + ) + + # Keep track of running total + n_items += jj + + # Now, there should be more stock items available! + response = self.get( + stock_url, + { + 'bom_item': bom_item.pk, + }, + expected_code=200 + ) + + self.assertEqual(len(response.data), n_items) + + # Now, disallow variant parts in the BomItem + bom_item.allow_variants = False + bom_item.save() + + # There should now only be 2 stock items available again + response = self.get( + stock_url, + { + 'bom_item': bom_item.pk, + }, + expected_code=200 + ) + + self.assertEqual(len(response.data), 2) + + def test_substitutes(self): + """ + Tests for BomItem substitutes + """ + + url = reverse('api-bom-substitute-list') + stock_url = reverse('api-stock-list') + + # Initially we have no substitute parts + response = self.get(url, expected_code=200) + self.assertEqual(len(response.data), 0) + + # BOM item we are interested in + bom_item = BomItem.objects.get(pk=1) + + # Filter stock items which can be assigned against this stock item + response = self.get( + stock_url, + { + "bom_item": bom_item.pk, + }, + expected_code=200 + ) + + n_items = len(response.data) + + loc = StockLocation.objects.get(pk=1) + + # Let's make some! + for ii in range(5): + sub_part = Part.objects.create( + name=f"Substitute {ii}", + description="A substitute part", + component=True, + is_template=False, + assembly=False + ) + + # Create a new StockItem for this Part + StockItem.objects.create( + part=sub_part, + quantity=1000, + location=loc, + ) + + # Now, create an "alternative" for the BOM Item + BomItemSubstitute.objects.create( + bom_item=bom_item, + part=sub_part + ) + + # We should be able to filter the API list to just return this new part + response = self.get(url, data={'part': sub_part.pk}, expected_code=200) + self.assertEqual(len(response.data), 1) + + # We should also have more stock available to allocate against this BOM item! + response = self.get( + stock_url, + { + "bom_item": bom_item.pk, + }, + expected_code=200 + ) + + self.assertEqual(len(response.data), n_items + ii + 1) + + # There should now be 5 substitute parts available in the database + response = self.get(url, expected_code=200) + self.assertEqual(len(response.data), 5) + + class PartParameterTest(InvenTreeAPITestCase): """ Tests for the ParParameter API From c7cec13076422bdbfc667f573a09c3b96339029b Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 23:18:44 +1100 Subject: [PATCH 16/22] Bug fix --- InvenTree/part/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index d6c1ba3741..ec377bd513 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1025,7 +1025,7 @@ class BomItemTest(InvenTreeAPITestCase): ) # Keep track of running total - n_items += jj + n_items += ii # Now, there should be more stock items available! response = self.get( From be5c5496b2ea4983c3869a8226e1cbb6ef35ebb8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 23:53:35 +1100 Subject: [PATCH 17/22] Improvements to allocation of stock items against build orders - Refactor functions for filtering stock using bom_item pk - Allow selection of substitute items when allocating against build order - Improvements for modal rendering - Don't display filter drop-down if there are no filters available --- InvenTree/build/models.py | 8 +-- InvenTree/build/templates/build/detail.html | 2 +- InvenTree/part/models.py | 51 ++++++++++++------- InvenTree/templates/js/translated/build.js | 19 ++++++- InvenTree/templates/js/translated/filters.js | 21 ++++---- .../js/translated/model_renderers.js | 45 ++++++++-------- InvenTree/templates/js/translated/part.js | 2 +- 7 files changed, 91 insertions(+), 57 deletions(-) diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 9a7b40b52f..8f74ba1c06 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -1153,16 +1153,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/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 8fb259f8a4..0b109d1890 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -197,7 +197,7 @@ -
+
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 7d0c87201c..fe3e017b28 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2333,6 +2333,38 @@ class BomItem(models.Model): def get_api_url(): return reverse('api-bom-list') + def get_valid_parts_for_allocation(self): + """ + Return a list of valid parts which can be allocated against this BomItem: + + - Include the referenced sub_part + - Include any directly specvified substitute parts + - If allow_variants is True, allow all variants of sub_part + """ + + # Set of parts we will allow + parts = set() + + parts.add(self.sub_part) + + # Variant parts (if allowed) + if self.allow_variants: + for variant in self.sub_part.get_descendants(include_self=False): + parts.add(variant) + + # Substitute parts + for sub in self.substitutes.all(): + parts.add(sub.part) + + return parts + + def is_stock_item_valid(self, stock_item): + """ + Check if the provided StockItem object is "valid" for assignment against this BomItem + """ + + return stock_item.part in self.get_valid_parts_for_allocation() + def get_stock_filter(self): """ Return a queryset filter for selecting StockItems which match this BomItem @@ -2342,24 +2374,7 @@ class BomItem(models.Model): """ - # List of parts we allow - part_ids = set() - - part_ids.add(self.sub_part.pk) - - # Variant parts (if allowed) - if self.allow_variants: - variants = self.sub_part.get_descendants(include_self=False) - - for v in variants: - part_ids.add(v.pk) - - # Direct substitute parts - for sub in self.substitutes.all(): - part_ids.add(sub.part.pk) - - # Return a list of Part ID values which can be filtered against - return Q(part__in=[pk for pk in part_ids]) + return Q(part__in=[part.pk for part in self.get_valid_parts_for_allocation()]) def save(self, *args, **kwargs): diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index d3be81e4e3..b1e517186a 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -348,6 +348,17 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { table = `#allocation-table-${outputId}`; } + // Filters + var filters = loadTableFilters('builditems'); + + var params = options.params || {}; + + for (var key in params) { + filters[key] = params[key]; + } + + setupFilterList('builditems', $(table), options.filterTarget || null); + // If an "output" is specified, then only "trackable" parts are allocated // Otherwise, only "untrackable" parts are allowed var trackable = ! !output; @@ -726,6 +737,10 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { html += makePartIcons(row.sub_part_detail); + if (row.substitutes && row.substitutes.length > 0) { + html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}'); + } + return html; } }, @@ -1021,12 +1036,12 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) { filters: { bom_item: bom_item.pk, in_stock: true, - part_detail: false, + part_detail: true, location_detail: true, }, model: 'stockitem', required: true, - render_part_detail: false, + render_part_detail: true, render_location_detail: true, auto_fill: true, adjustFilters: function(filters) { diff --git a/InvenTree/templates/js/translated/filters.js b/InvenTree/templates/js/translated/filters.js index 3e41003696..8845fc39e1 100644 --- a/InvenTree/templates/js/translated/filters.js +++ b/InvenTree/templates/js/translated/filters.js @@ -283,18 +283,21 @@ function setupFilterList(tableKey, table, target) { element.append(``); - element.append(``); + // If there are available filters, add them in! + if (filters.length > 0) { + element.append(``); - if (Object.keys(filters).length > 0) { - element.append(``); - } + if (Object.keys(filters).length > 0) { + element.append(``); + } - for (var key in filters) { - var value = getFilterOptionValue(tableKey, key, filters[key]); - var title = getFilterTitle(tableKey, key); - var description = getFilterDescription(tableKey, key); + for (var key in filters) { + var value = getFilterOptionValue(tableKey, key, filters[key]); + var title = getFilterTitle(tableKey, key); + var description = getFilterDescription(tableKey, key); - element.append(`
${title} = ${value}x
`); + element.append(`
${title} = ${value}x
`); + } } // Callback for reloading the table diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index cf675ff4f7..0bb0818a70 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -52,8 +52,6 @@ function renderStockItem(name, data, parameters, options) { if (data.part_detail) { image = data.part_detail.thumbnail || data.part_detail.image || blankImage(); } - - var html = ''; var render_part_detail = true; @@ -61,23 +59,10 @@ function renderStockItem(name, data, parameters, options) { render_part_detail = parameters['render_part_detail']; } + var part_detail = ''; + if (render_part_detail) { - html += ``; - html += ` ${data.part_detail.full_name || data.part_detail.name}`; - } - - html += ''; - - if (data.serial && data.quantity == 1) { - html += `{% trans "Serial Number" %}: ${data.serial}`; - } else { - html += `{% trans "Quantity" %}: ${data.quantity}`; - } - - html += ''; - - if (render_part_detail && data.part_detail.description) { - html += `

${data.part_detail.description}

`; + part_detail = `${data.part_detail.full_name} - `; } var render_stock_id = true; @@ -86,8 +71,10 @@ function renderStockItem(name, data, parameters, options) { render_stock_id = parameters['render_stock_id']; } + var stock_id = ''; + if (render_stock_id) { - html += `{% trans "Stock ID" %}: ${data.pk}`; + stock_id = `{% trans "Stock ID" %}: ${data.pk}`; } var render_location_detail = false; @@ -96,10 +83,28 @@ function renderStockItem(name, data, parameters, options) { render_location_detail = parameters['render_location_detail']; } + var location_detail = ''; + if (render_location_detail && data.location_detail) { - html += ` - ${data.location_detail.name}`; + location_detail = ` - (${data.location_detail.name})`; } + var stock_detail = ''; + + if (data.serial && data.quantity == 1) { + stock_detail = `{% trans "Serial Number" %}: ${data.serial}`; + } else if (data.quantity == 0) { + stock_detail = `{% trans "No Stock"% }`; + } else { + stock_detail = `{% trans "Quantity" %}: ${data.quantity}`; + } + + var html = ` + + ${part_detail}${stock_detail}${location_detail}${stock_id} + + `; + return html; } diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index aba3c46330..037df8d607 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -592,7 +592,7 @@ function loadPartParameterTable(table, url, options) { filters[key] = params[key]; } - // setupFilterLsit("#part-parameters", $(table)); + // setupFilterList("#part-parameters", $(table)); $(table).inventreeTable({ url: url, From 890741ef5d00e1b4507863345446b10116ccc472 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 13 Oct 2021 23:57:50 +1100 Subject: [PATCH 18/22] Display badge indicating variant stock can be used --- InvenTree/templates/js/translated/bom.js | 4 ++++ InvenTree/templates/js/translated/build.js | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js index 9d3380c653..048747c6bb 100644 --- a/InvenTree/templates/js/translated/bom.js +++ b/InvenTree/templates/js/translated/bom.js @@ -378,6 +378,10 @@ function loadBomTable(table, options) { html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}'); } + if (row.allow_variants) { + html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}'); + } + // Display an extra icon if this part is an assembly if (sub_part.assembly) { var text = ``; diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index b1e517186a..4dad5dc454 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -738,7 +738,11 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { html += makePartIcons(row.sub_part_detail); if (row.substitutes && row.substitutes.length > 0) { - html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}'); + html += makeIconBadge('fa-exchange-alt', '{% trans "Substitute parts available" %}'); + } + + if (row.allow_variants) { + html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}'); } return html; From 0581885ad5983622fb0aaf8f7e71c60d21bb07df Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 14 Oct 2021 00:26:34 +1100 Subject: [PATCH 19/22] Simplify BOM table buttons --- InvenTree/part/templates/part/bom.html | 55 ++++++++++--------- .../stock/templates/stock/item_base.html | 4 +- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/InvenTree/part/templates/part/bom.html b/InvenTree/part/templates/part/bom.html index 6808630ed1..988f65cfdb 100644 --- a/InvenTree/part/templates/part/bom.html +++ b/InvenTree/part/templates/part/bom.html @@ -5,7 +5,8 @@
{% trans "You do not have permission to edit the BOM." %}
-{% else %} +{% endif %} + {% if part.bom_checked_date %} {% if part.is_bom_valid %}
@@ -23,34 +24,38 @@
+ + {% if roles.part.change %} - - - {% if part.variant_of %} - - {% endif %} + +
+ + +
- {% if part.is_bom_valid == False %} - {% endif %} - {% endif %} - {% endif %} - - +
diff --git a/InvenTree/stock/templates/stock/item_base.html b/InvenTree/stock/templates/stock/item_base.html index 798802ab81..00891d7b6a 100644 --- a/InvenTree/stock/templates/stock/item_base.html +++ b/InvenTree/stock/templates/stock/item_base.html @@ -72,7 +72,9 @@ {% if barcodes %}
- +