mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2761 from SchrodingersGat/category-parameters
Category parameters
This commit is contained in:
commit
96d89bf4ae
@ -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 ¶meters=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
|
||||||
|
|
||||||
|
@ -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 = ''
|
||||||
|
@ -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
|
||||||
|
@ -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 """
|
||||||
|
|
||||||
|
@ -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 %}
|
||||||
|
@ -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)
|
||||||
|
@ -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');
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user