Merge pull request #2761 from SchrodingersGat/category-parameters

Category parameters
This commit is contained in:
Oliver 2022-03-19 22:48:53 +11:00 committed by GitHub
commit 96d89bf4ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 230 additions and 160 deletions

View File

@ -12,11 +12,16 @@ import common.models
INVENTREE_SW_VERSION = "0.7.0 dev" INVENTREE_SW_VERSION = "0.7.0 dev"
# InvenTree API version # 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 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 &parameters=true)
- Adds ability to filter PartParameterTemplate API by Part instance
- Adds ability to filter PartParameterTemplate API by PartCategory instance
v31 -> 2022-03-14 v31 -> 2022-03-14
- Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints - Adds "updated" field to SupplierPriceBreakList and SupplierPriceBreakDetail API endpoints

View File

@ -17,7 +17,7 @@ def currency_code_default():
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
try: try:
code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY') code = InvenTreeSetting.get_setting('INVENTREE_DEFAULT_CURRENCY', create=False)
except ProgrammingError: # pragma: no cover except ProgrammingError: # pragma: no cover
# database is not initialized yet # database is not initialized yet
code = '' code = ''

View File

@ -855,6 +855,14 @@ class PartList(generics.ListCreateAPIView):
kwargs['starred_parts'] = self.starred_parts 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) return self.serializer_class(*args, **kwargs)
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
@ -1405,6 +1413,44 @@ class PartParameterTemplateList(generics.ListCreateAPIView):
'name', '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): class PartParameterList(generics.ListCreateAPIView):
""" API endpoint for accessing a list of PartParameter objects """ API endpoint for accessing a list of PartParameter objects

View File

@ -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): class PartBriefSerializer(InvenTreeModelSerializer):
""" Serializer for Part (brief detail) """ """ Serializer for Part (brief detail) """
@ -259,11 +287,16 @@ class PartSerializer(InvenTreeModelSerializer):
category_detail = kwargs.pop('category_detail', False) category_detail = kwargs.pop('category_detail', False)
parameters = kwargs.pop('parameters', False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if category_detail is not True: if category_detail is not True:
self.fields.pop('category_detail') self.fields.pop('category_detail')
if parameters is not True:
self.fields.pop('parameters')
@staticmethod @staticmethod
def annotate_queryset(queryset): def annotate_queryset(queryset):
""" """
@ -356,19 +389,18 @@ class PartSerializer(InvenTreeModelSerializer):
# PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...) # PrimaryKeyRelated fields (Note: enforcing field type here results in much faster queries, somehow...)
category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all()) category = serializers.PrimaryKeyRelatedField(queryset=PartCategory.objects.all())
# TODO - Include annotation for the following fields: parameters = PartParameterSerializer(
# allocated_stock = serializers.FloatField(source='allocation_count', read_only=True) many=True,
# bom_items = serializers.IntegerField(source='bom_count', read_only=True) read_only=True,
# used_in = serializers.IntegerField(source='used_in_count', read_only=True) )
class Meta: class Meta:
model = Part model = Part
partial = True partial = True
fields = [ fields = [
'active', 'active',
# 'allocated_stock',
'assembly', 'assembly',
# 'bom_items',
'category', 'category',
'category_detail', 'category_detail',
'component', 'component',
@ -388,6 +420,7 @@ class PartSerializer(InvenTreeModelSerializer):
'minimum_stock', 'minimum_stock',
'name', 'name',
'notes', 'notes',
'parameters',
'pk', 'pk',
'purchaseable', 'purchaseable',
'revision', 'revision',
@ -398,7 +431,6 @@ class PartSerializer(InvenTreeModelSerializer):
'thumbnail', 'thumbnail',
'trackable', 'trackable',
'units', 'units',
# 'used_in',
'variant_of', 'variant_of',
'virtual', '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): class CategoryParameterTemplateSerializer(InvenTreeModelSerializer):
""" Serializer for PartCategoryParameterTemplate """ """ Serializer for PartCategoryParameterTemplate """

View File

@ -223,13 +223,14 @@
{{ block.super }} {{ block.super }}
{% if category %} {% if category %}
loadParametricPartTable( onPanelLoad('parameters', function() {
"#parametric-part-table", loadParametricPartTable(
{ "#parametric-part-table",
headers: {{ headers|safe }}, {
data: {{ parameters|safe }}, category: {{ category.pk }},
} }
); );
});
$("#toggle-starred").click(function() { $("#toggle-starred").click(function() {
toggleStar({ toggleStar({
@ -240,9 +241,6 @@
{% endif %} {% endif %}
// Enable left-hand navigation sidebar
enableSidebar('category');
// Enable breadcrumb tree view // Enable breadcrumb tree view
enableBreadcrumbTree({ enableBreadcrumbTree({
label: 'category', label: 'category',
@ -258,18 +256,20 @@
} }
}); });
loadPartCategoryTable( onPanelLoad('subcategories', function() {
$('#subcategory-table'), { loadPartCategoryTable(
params: { $('#subcategory-table'), {
{% if category %} params: {
parent: {{ category.pk }}, {% if category %}
{% else %} parent: {{ category.pk }},
parent: null, {% else %}
{% endif %} parent: null,
}, {% endif %}
allowTreeView: true, },
} allowTreeView: true,
); }
);
});
$("#cat-create").click(function() { $("#cat-create").click(function() {
@ -339,19 +339,24 @@
{% endif %} {% endif %}
loadPartTable( onPanelLoad('parts', function() {
"#part-table", loadPartTable(
"{% url 'api-part-list' %}", "#part-table",
{ "{% url 'api-part-list' %}",
params: { {
{% if category %}category: {{ category.id }}, params: {
{% else %}category: "null", {% if category %}category: {{ category.id }},
{% endif %} {% 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 %} {% endblock %}

View File

@ -988,22 +988,6 @@ class CategoryDetail(InvenTreeRoleMixin, DetailView):
category = kwargs.get('object', None) category = kwargs.get('object', None)
if category: 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 # Insert "starred" information
context['starred'] = category.is_starred_by(self.request.user) context['starred'] = category.is_starred_by(self.request.user)

View File

@ -202,15 +202,17 @@
{% block js_ready %} {% block js_ready %}
{{ block.super }} {{ block.super }}
loadStockLocationTable($('#sublocation-table'), { onPanelLoad('sublocations', function() {
params: { loadStockLocationTable($('#sublocation-table'), {
{% if location %} params: {
parent: {{ location.pk }}, {% if location %}
{% else %} parent: {{ location.pk }},
parent: 'null', {% else %}
{% endif %} parent: 'null',
}, {% endif %}
allowTreeView: true, },
allowTreeView: true,
});
}); });
linkButtonsToSelection( linkButtonsToSelection(
@ -325,19 +327,21 @@
}); });
}); });
loadStockTable($("#stock-table"), { onPanelLoad('stock', function() {
buttons: [ loadStockTable($("#stock-table"), {
'#stock-options', buttons: [
], '#stock-options',
params: { ],
{% if location %} params: {
location: {{ location.pk }}, {% if location %}
{% endif %} location: {{ location.pk }},
part_detail: true, {% endif %}
location_detail: true, part_detail: true,
supplier_part_detail: true, location_detail: true,
}, supplier_part_detail: true,
url: "{% url 'api-stock-list' %}", },
url: "{% url 'api-stock-list' %}",
});
}); });
enableSidebar('stocklocation'); enableSidebar('stocklocation');

View File

@ -312,7 +312,13 @@ function renderPartCategory(name, data, parameters, options) {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
function renderPartParameterTemplate(name, data, parameters, options) { function renderPartParameterTemplate(name, data, parameters, options) {
var html = `<span>${data.name} - [${data.units}]</span>`; var units = '';
if (data.units) {
units = ` [${data.units}]`;
}
var html = `<span>${data.name}${units}</span>`;
return html; return html;
} }

View File

@ -1068,68 +1068,84 @@ function loadRelatedPartsTable(table, part_id, options={}) {
} }
/* Load parametric table for part parameters
*/
function loadParametricPartTable(table, options={}) { 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 columns = [
var table_data = options.data; {
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) { return display;
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',
});
} }
} ];
// 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({ $(table).inventreeTable({
sortName: 'part', url: '{% url "api-part-list" %}',
queryParams: table_headers, queryParams: {
category: options.category,
cascade: true,
parameters: true,
},
groupBy: false, groupBy: false,
name: options.name || 'parametric', name: options.name || 'part-parameters',
formatNoMatches: function() { formatNoMatches: function() {
return '{% trans "No parts found" %}'; return '{% trans "No parts found" %}';
}, },
columns: columns, columns: columns,
showColumns: true, 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);
}
}
}); });
} }