mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1008 from eeintech/parametric_part_tables
Add parametric part tables to category detail page
This commit is contained in:
commit
7f3018ebf8
13
InvenTree/InvenTree/static/css/bootstrap-table-filter-control.css
vendored
Normal file
13
InvenTree/InvenTree/static/css/bootstrap-table-filter-control.css
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
@charset "UTF-8";
|
||||
/**
|
||||
* @author: Dennis Hernández
|
||||
* @webSite: http://djhvscf.github.io/Blog
|
||||
* @version: v2.1.1
|
||||
*/
|
||||
.no-filter-control {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.filter-control {
|
||||
margin: 0 2px 2px 2px;
|
||||
}
|
3021
InvenTree/InvenTree/static/script/bootstrap/bootstrap-table-filter-control.js
vendored
Normal file
3021
InvenTree/InvenTree/static/script/bootstrap/bootstrap-table-filter-control.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
2361
InvenTree/InvenTree/static/script/bootstrap/filter-control-utils.js
Normal file
2361
InvenTree/InvenTree/static/script/bootstrap/filter-control-utils.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -111,6 +111,58 @@ class PartCategory(InvenTreeTree):
|
||||
""" True if there are any parts in this category """
|
||||
return self.partcount() > 0
|
||||
|
||||
def prefetch_parts_parameters(self, cascade=True):
|
||||
""" Prefectch parts parameters """
|
||||
|
||||
return self.get_parts(cascade=cascade).prefetch_related('parameters', 'parameters__template').all()
|
||||
|
||||
def get_unique_parameters(self, cascade=True, prefetch=None):
|
||||
""" Get all unique parameter names for all parts from this category """
|
||||
|
||||
unique_parameters_names = []
|
||||
|
||||
if prefetch:
|
||||
parts = prefetch
|
||||
else:
|
||||
parts = self.prefetch_parts_parameters(cascade=cascade)
|
||||
|
||||
for part in parts:
|
||||
for parameter in part.parameters.all():
|
||||
parameter_name = parameter.template.name
|
||||
if parameter_name not in unique_parameters_names:
|
||||
unique_parameters_names.append(parameter_name)
|
||||
|
||||
return sorted(unique_parameters_names)
|
||||
|
||||
def get_parts_parameters(self, cascade=True, prefetch=None):
|
||||
""" Get all parameter names and values for all parts from this category """
|
||||
|
||||
category_parameters = []
|
||||
|
||||
if prefetch:
|
||||
parts = prefetch
|
||||
else:
|
||||
parts = self.prefetch_parts_parameters(cascade=cascade)
|
||||
|
||||
for part in parts:
|
||||
part_parameters = {
|
||||
'pk': part.pk,
|
||||
'name': part.name,
|
||||
'description': part.description,
|
||||
}
|
||||
# Add IPN only if it exists
|
||||
if part.IPN:
|
||||
part_parameters['IPN'] = part.IPN
|
||||
|
||||
for parameter in part.parameters.all():
|
||||
parameter_name = parameter.template.name
|
||||
parameter_value = parameter.data
|
||||
part_parameters[parameter_name] = parameter_value
|
||||
|
||||
category_parameters.append(part_parameters)
|
||||
|
||||
return category_parameters
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
|
||||
def before_delete_part_category(sender, instance, using, **kwargs):
|
||||
|
@ -120,8 +120,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% block category_tables %}
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||
</table>
|
||||
{% endblock category_tables %}
|
||||
|
||||
{% endblock %}
|
||||
{% block js_load %}
|
||||
|
31
InvenTree/part/templates/part/category_parametric.html
Normal file
31
InvenTree/part/templates/part/category_parametric.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% extends "part/category.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block category_tables %}
|
||||
|
||||
{% include 'part/category_tabs.html' with tab='parametric-table' %}
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='parametric-part-table'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block js_ready %}
|
||||
{{ block.super }}
|
||||
|
||||
/* Hide Button Toolbar */
|
||||
window.onload = function hideButtonToolbar() {
|
||||
var toolbar = document.getElementById("button-toolbar");
|
||||
toolbar.style.display = "none";
|
||||
};
|
||||
|
||||
loadParametricPartTable(
|
||||
"#parametric-part-table",
|
||||
{
|
||||
headers: {{ headers|safe }},
|
||||
data: {{ parameters|safe }},
|
||||
}
|
||||
);
|
||||
|
||||
{% endblock %}
|
12
InvenTree/part/templates/part/category_partlist.html
Normal file
12
InvenTree/part/templates/part/category_partlist.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "part/category.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block category_tables %}
|
||||
|
||||
{% include 'part/category_tabs.html' with tab='part-list' %}
|
||||
|
||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
11
InvenTree/part/templates/part/category_tabs.html
Normal file
11
InvenTree/part/templates/part/category_tabs.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li{% ifequal tab 'part-list' %} class="active"{% endifequal %}>
|
||||
<a href="{% url 'category-detail' category.id %}">{% trans "Parts" %} <span class="badge">{% decimal part_count %}</span></a>
|
||||
</li>
|
||||
<li{% ifequal tab 'parametric-table' %} class='active'{% endifequal %}>
|
||||
<a href="{% url 'category-parametric' category.id %}">{% trans "Parametric Table" %}</a>
|
||||
</li>
|
||||
</ul>
|
@ -1,7 +1,7 @@
|
||||
from django.test import TestCase
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from .models import Part, PartCategory
|
||||
from .models import Part, PartCategory, PartParameter, PartParameterTemplate
|
||||
|
||||
|
||||
class CategoryTest(TestCase):
|
||||
@ -15,6 +15,7 @@ class CategoryTest(TestCase):
|
||||
'category',
|
||||
'part',
|
||||
'location',
|
||||
'params',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
@ -94,6 +95,31 @@ class CategoryTest(TestCase):
|
||||
|
||||
self.assertEqual(self.electronics.item_count, self.electronics.partcount())
|
||||
|
||||
def test_parameters(self):
|
||||
""" Test that the Category parameters are correctly fetched """
|
||||
|
||||
# Check number of SQL queries to iterate other parameters
|
||||
with self.assertNumQueries(3):
|
||||
# Prefetch: 3 queries (parts, parameters and parameters_template)
|
||||
fasteners = self.fasteners.prefetch_parts_parameters()
|
||||
# Iterate through all parts and parameters
|
||||
for fastener in fasteners:
|
||||
self.assertIsInstance(fastener, Part)
|
||||
for parameter in fastener.parameters.all():
|
||||
self.assertIsInstance(parameter, PartParameter)
|
||||
self.assertIsInstance(parameter.template, PartParameterTemplate)
|
||||
|
||||
# Test number of unique parameters
|
||||
self.assertEqual(len(self.fasteners.get_unique_parameters(prefetch=fasteners)), 1)
|
||||
# Test number of parameters found for each part
|
||||
parts_parameters = self.fasteners.get_parts_parameters(prefetch=fasteners)
|
||||
part_infos = ['pk', 'name', 'description']
|
||||
for part_parameter in parts_parameters:
|
||||
# Remove part informations
|
||||
for item in part_infos:
|
||||
part_parameter.pop(item)
|
||||
self.assertEqual(len(part_parameter), 1)
|
||||
|
||||
def test_invalid_name(self):
|
||||
# Test that an illegal character is prohibited in a category name
|
||||
|
||||
|
@ -77,7 +77,8 @@ part_category_urls = [
|
||||
url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
|
||||
url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'),
|
||||
|
||||
url('^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||
]
|
||||
|
||||
part_bom_urls = [
|
||||
|
@ -1872,10 +1872,51 @@ class PartParameterDelete(AjaxDeleteView):
|
||||
|
||||
class CategoryDetail(DetailView):
|
||||
""" Detail view for PartCategory """
|
||||
|
||||
model = PartCategory
|
||||
context_object_name = 'category'
|
||||
queryset = PartCategory.objects.all().prefetch_related('children')
|
||||
template_name = 'part/category.html'
|
||||
template_name = 'part/category_partlist.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super(CategoryDetail, self).get_context_data(**kwargs).copy()
|
||||
|
||||
try:
|
||||
context['part_count'] = kwargs['object'].partcount()
|
||||
except KeyError:
|
||||
context['part_count'] = 0
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class CategoryParametric(CategoryDetail):
|
||||
""" Parametric view for PartCategory """
|
||||
|
||||
template_name = 'part/category_parametric.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super(CategoryParametric, self).get_context_data(**kwargs).copy()
|
||||
|
||||
# Get current category
|
||||
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)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class CategoryEdit(AjaxUpdateView):
|
||||
|
@ -39,6 +39,7 @@
|
||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-toggle.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table-filter-control.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
<link rel="stylesheet" href="{% get_color_theme_css user.get_username %}">
|
||||
|
||||
@ -99,6 +100,8 @@ InvenTree
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-en-US.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-group-by.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-toggle.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-filter-control.js' %}"></script>
|
||||
<!-- <script type='text/javascript' src="{% static 'script/bootstrap/filter-control-utils.js' %}"></script> -->
|
||||
|
||||
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
|
||||
|
@ -163,6 +163,72 @@ function loadSimplePartTable(table, url, 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 table_data = options.data
|
||||
|
||||
var columns = [];
|
||||
|
||||
for (header of table_headers) {
|
||||
if (header === 'part') {
|
||||
columns.push({
|
||||
field: header,
|
||||
title: '{% trans 'Part' %}',
|
||||
sortable: true,
|
||||
sortName: 'name',
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
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',
|
||||
/* TODO: Search icons are not displayed */
|
||||
/*clear: 'fa-times icon-red',*/
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(table).inventreeTable({
|
||||
sortName: 'part',
|
||||
queryParams: table_headers,
|
||||
groupBy: false,
|
||||
name: options.name || 'parametric',
|
||||
formatNoMatches: function() { return "{% trans "No parts found" %}"; },
|
||||
columns: columns,
|
||||
showColumns: true,
|
||||
data: table_data,
|
||||
filterControl: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadPartTable(table, url, options={}) {
|
||||
/* Load part listing data into specified table.
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user