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 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 &parameters=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

View File

@ -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 = ''

View File

@ -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

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):
""" 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 """

View File

@ -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 %}

View File

@ -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)

View File

@ -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');

View File

@ -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 = `<span>${data.name} - [${data.units}]</span>`;
var units = '';
if (data.units) {
units = ` [${data.units}]`;
}
var html = `<span>${data.name}${units}</span>`;
return html;
}

View File

@ -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);
}
}
});
}