diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 85dc275690..6d4848f436 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,16 @@ import common.models INVENTREE_SW_VERSION = "0.7.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 31 +INVENTREE_API_VERSION = 32 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v32 -> 2022-03-19 + - Adds "parameters" detail to Part API endpoint (use ¶meters=true) + - Adds ability to filter PartParameterTemplate API by Part instance + - Adds ability to filter PartParameterTemplate API by PartCategory instance + v31 -> 2022-03-14 - Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index 6973f9ecd3..2dd7756eba 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -17,7 +17,7 @@ def currency_code_default(): from common.models import InvenTreeSetting try: - code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') + code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', create=False) except ProgrammingError: # pragma: no cover # database is not initialized yet code = '' diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index f542e36c68..3a2bb6eeb3 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -855,6 +855,14 @@ class PartList(generics.ListCreateAPIView): kwargs['starred_parts'] = self.starred_parts + try: + params = self.request.query_params + + kwargs['parameters'] = str2bool(params.get('parameters', None)) + + except AttributeError: + pass + return self.serializer_class(*args, **kwargs) def list(self, request, *args, **kwargs): @@ -1405,6 +1413,44 @@ class PartParameterTemplateList(generics.ListCreateAPIView): 'name', ] + def filter_queryset(self, queryset): + """ + Custom filtering for the PartParameterTemplate API + """ + + queryset = super().filter_queryset(queryset) + + params = self.request.query_params + + # Filtering against a "Part" - return only parameter templates which are referenced by a part + part = params.get('part', None) + + if part is not None: + + try: + part = Part.objects.get(pk=part) + parameters = PartParameter.objects.filter(part=part) + template_ids = parameters.values_list('template').distinct() + queryset = queryset.filter(pk__in=[el[0] for el in template_ids]) + except (ValueError, Part.DoesNotExist): + pass + + # Filtering against a "PartCategory" - return only parameter templates which are referenced by parts in this category + category = params.get('category', None) + + if category is not None: + + try: + category = PartCategory.objects.get(pk=category) + cats = category.get_descendants(include_self=True) + parameters = PartParameter.objects.filter(part__category__in=cats) + template_ids = parameters.values_list('template').distinct() + queryset = queryset.filter(pk__in=[el[0] for el in template_ids]) + except (ValueError, PartCategory.DoesNotExist): + pass + + return queryset + class PartParameterList(generics.ListCreateAPIView): """ API endpoint for accessing a list of PartParameter objects diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py index 5d63448bb7..c46950adca 100644 --- a/InvenTree/part/serializers.py +++ b/InvenTree/part/serializers.py @@ -211,6 +211,34 @@ class PartThumbSerializerUpdate(InvenTreeModelSerializer): ] +class PartParameterTemplateSerializer(InvenTreeModelSerializer): + """ JSON serializer for the PartParameterTemplate model """ + + class Meta: + model = PartParameterTemplate + fields = [ + 'pk', + 'name', + 'units', + ] + + +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 PartBriefSerializer(InvenTreeModelSerializer): """ Serializer for Part (brief detail) """ @@ -259,11 +287,16 @@ class PartSerializer(InvenTreeModelSerializer): category_detail = kwargs.pop('category_detail', False) + parameters = kwargs.pop('parameters', False) + super().__init__(*args, **kwargs) if category_detail is not True: self.fields.pop('category_detail') + if parameters is not True: + self.fields.pop('parameters') + @staticmethod def annotate_queryset(queryset): """ @@ -356,19 +389,18 @@ class PartSerializer(InvenTreeModelSerializer): # PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...) category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all()) - # TODO - Include annotation for the following fields: - # allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) - # bom_items = serializers.IntegerField(source='bom_count', read_only=True) - # used_in = serializers.IntegerField(source='used_in_count', read_only=True) + parameters = PartParameterSerializer( + many=True, + read_only=True, + ) class Meta: model = Part partial = True fields = [ 'active', - # 'allocated_stock', + 'assembly', - # 'bom_items', 'category', 'category_detail', 'component', @@ -388,6 +420,7 @@ class PartSerializer(InvenTreeModelSerializer): 'minimum_stock', 'name', 'notes', + 'parameters', 'pk', 'purchaseable', 'revision', @@ -398,7 +431,6 @@ class PartSerializer(InvenTreeModelSerializer): 'thumbnail', 'trackable', 'units', - # 'used_in', 'variant_of', 'virtual', ] @@ -600,34 +632,6 @@ class BomItemSerializer(InvenTreeModelSerializer): ] -class PartParameterTemplateSerializer(InvenTreeModelSerializer): - """ JSON serializer for the PartParameterTemplate model """ - - class Meta: - model = PartParameterTemplate - fields = [ - 'pk', - 'name', - 'units', - ] - - -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/category.html b/InvenTree/part/templates/part/category.html index acbd0b16f1..6a61ef2fbf 100644 --- a/InvenTree/part/templates/part/category.html +++ b/InvenTree/part/templates/part/category.html @@ -223,13 +223,14 @@ {{ block.super }} {% if category %} - loadParametricPartTable( - "#parametric-part-table", - { - headers: {{ headers|safe }}, - data: {{ parameters|safe }}, - } - ); + onPanelLoad('parameters', function() { + loadParametricPartTable( + "#parametric-part-table", + { + category: {{ category.pk }}, + } + ); + }); $("#toggle-starred").click(function() { toggleStar({ @@ -240,9 +241,6 @@ {% endif %} - // Enable left-hand navigation sidebar - enableSidebar('category'); - // Enable breadcrumb tree view enableBreadcrumbTree({ label: 'category', @@ -258,18 +256,20 @@ } }); - loadPartCategoryTable( - $('#subcategory-table'), { - params: { - {% if category %} - parent: {{ category.pk }}, - {% else %} - parent: null, - {% endif %} - }, - allowTreeView: true, - } - ); + onPanelLoad('subcategories', function() { + loadPartCategoryTable( + $('#subcategory-table'), { + params: { + {% if category %} + parent: {{ category.pk }}, + {% else %} + parent: null, + {% endif %} + }, + allowTreeView: true, + } + ); + }); $("#cat-create").click(function() { @@ -339,19 +339,24 @@ {% endif %} - loadPartTable( - "#part-table", - "{% url 'api-part-list' %}", - { - params: { - {% if category %}category: {{ category.id }}, - {% else %}category: "null", - {% endif %} + onPanelLoad('parts', function() { + loadPartTable( + "#part-table", + "{% url 'api-part-list' %}", + { + params: { + {% if category %}category: {{ category.id }}, + {% else %}category: "null", + {% endif %} + }, + buttons: ['#part-options'], + checkbox: true, + gridView: true, }, - buttons: ['#part-options'], - checkbox: true, - gridView: true, - }, - ); + ); + }); + + // Enable left-hand navigation sidebar + enableSidebar('category'); {% endblock %} diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index fb45db8f07..9f3cd07f7c 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -988,22 +988,6 @@ class CategoryDetail(InvenTreeRoleMixin, DetailView): category = kwargs.get('object', None) if category: - cascade = kwargs.get('cascade', True) - - # Prefetch parts parameters - parts_parameters = category.prefetch_parts_parameters(cascade=cascade) - - # Get table headers (unique parameters names) - context['headers'] = category.get_unique_parameters(cascade=cascade, - prefetch=parts_parameters) - - # Insert part information - context['headers'].insert(0, 'description') - context['headers'].insert(0, 'part') - - # Get parameters data - context['parameters'] = category.get_parts_parameters(cascade=cascade, - prefetch=parts_parameters) # Insert "starred" information context['starred'] = category.is_starred_by(self.request.user) diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html index 1da0030bc6..45dcc0ba59 100644 --- a/InvenTree/stock/templates/stock/location.html +++ b/InvenTree/stock/templates/stock/location.html @@ -202,15 +202,17 @@ {% block js_ready %} {{ block.super }} - loadStockLocationTable($('#sublocation-table'), { - params: { - {% if location %} - parent: {{ location.pk }}, - {% else %} - parent: 'null', - {% endif %} - }, - allowTreeView: true, + onPanelLoad('sublocations', function() { + loadStockLocationTable($('#sublocation-table'), { + params: { + {% if location %} + parent: {{ location.pk }}, + {% else %} + parent: 'null', + {% endif %} + }, + allowTreeView: true, + }); }); linkButtonsToSelection( @@ -325,19 +327,21 @@ }); }); - loadStockTable($("#stock-table"), { - buttons: [ - '#stock-options', - ], - params: { - {% if location %} - location: {{ location.pk }}, - {% endif %} - part_detail: true, - location_detail: true, - supplier_part_detail: true, - }, - url: "{% url 'api-stock-list' %}", + onPanelLoad('stock', function() { + loadStockTable($("#stock-table"), { + buttons: [ + '#stock-options', + ], + params: { + {% if location %} + location: {{ location.pk }}, + {% endif %} + part_detail: true, + location_detail: true, + supplier_part_detail: true, + }, + url: "{% url 'api-stock-list' %}", + }); }); enableSidebar('stocklocation'); diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index 68ba496309..e3abe1186f 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -312,7 +312,13 @@ function renderPartCategory(name, data, parameters, options) { // eslint-disable-next-line no-unused-vars function renderPartParameterTemplate(name, data, parameters, options) { - var html = `${data.name} - [${data.units}]`; + var units = ''; + + if (data.units) { + units = ` [${data.units}]`; + } + + var html = `${data.name}${units}`; return html; } diff --git a/InvenTree/templates/js/translated/part.js b/InvenTree/templates/js/translated/part.js index dcdd3da7d6..b0283a1b35 100644 --- a/InvenTree/templates/js/translated/part.js +++ b/InvenTree/templates/js/translated/part.js @@ -1068,68 +1068,84 @@ function loadRelatedPartsTable(table, part_id, options={}) { } +/* Load parametric table for part parameters + */ function loadParametricPartTable(table, options={}) { - /* Load parametric table for part parameters - * - * Args: - * - table: HTML reference to the table - * - table_headers: Unique parameters found in category - * - table_data: Parameters data - */ - var table_headers = options.headers; - var table_data = options.data; + var columns = [ + { + field: 'name', + title: '{% trans "Part" %}', + switchable: false, + sortable: true, + formatter: function(value, row) { + var name = row.full_name; - var columns = []; + var display = imageHoverIcon(row.thumbnail) + renderLink(name, `/part/${row.pk}/`); - for (var header of table_headers) { - if (header === 'part') { - columns.push({ - field: header, - title: '{% trans "Part" %}', - sortable: true, - sortName: 'name', - formatter: function(value, row) { - - var name = ''; - - if (row.IPN) { - name += row.IPN + ' | ' + row.name; - } else { - name += row.name; - } - - return renderLink(name, '/part/' + row.pk + '/'); - } - }); - } else if (header === 'description') { - columns.push({ - field: header, - title: '{% trans "Description" %}', - sortable: true, - }); - } else { - columns.push({ - field: header, - title: header, - sortable: true, - filterControl: 'input', - }); + return display; + } } - } + ]; + + // Request a list of parameters we are interested in for this category + inventreeGet( + '{% url "api-part-parameter-template-list" %}', + { + category: options.category, + }, + { + async: false, + success: function(response) { + for (var template of response) { + columns.push({ + field: `parameter_${template.pk}`, + title: template.name, + switchable: true, + sortable: true, + filterControl: 'input', + }); + } + } + } + ); + + // TODO: Re-enable filter control for parameter values $(table).inventreeTable({ - sortName: 'part', - queryParams: table_headers, + url: '{% url "api-part-list" %}', + queryParams: { + category: options.category, + cascade: true, + parameters: true, + }, groupBy: false, - name: options.name || 'parametric', + name: options.name || 'part-parameters', formatNoMatches: function() { return '{% trans "No parts found" %}'; }, columns: columns, showColumns: true, - data: table_data, - filterControl: true, + // filterControl: true, + sidePagination: 'server', + idField: 'pk', + uniqueId: 'pk', + onLoadSuccess: function() { + + var data = $(table).bootstrapTable('getData'); + + for (var idx = 0; idx < data.length; idx++) { + var row = data[idx]; + var pk = row.pk; + + // Make each parameter accessible, based on the "template" columns + row.parameters.forEach(function(parameter) { + row[`parameter_${parameter.template}`] = parameter.data; + }); + + $(table).bootstrapTable('updateRow', pk, row); + } + } }); }