mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +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 """
|
""" True if there are any parts in this category """
|
||||||
return self.partcount() > 0
|
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')
|
@receiver(pre_delete, sender=PartCategory, dispatch_uid='partcategory_delete_log')
|
||||||
def before_delete_part_category(sender, instance, using, **kwargs):
|
def before_delete_part_category(sender, instance, using, **kwargs):
|
||||||
|
@ -120,8 +120,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% block category_tables %}
|
||||||
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='part-table'>
|
||||||
</table>
|
</table>
|
||||||
|
{% endblock category_tables %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block js_load %}
|
{% 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.test import TestCase
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
from .models import Part, PartCategory
|
from .models import Part, PartCategory, PartParameter, PartParameterTemplate
|
||||||
|
|
||||||
|
|
||||||
class CategoryTest(TestCase):
|
class CategoryTest(TestCase):
|
||||||
@ -15,6 +15,7 @@ class CategoryTest(TestCase):
|
|||||||
'category',
|
'category',
|
||||||
'part',
|
'part',
|
||||||
'location',
|
'location',
|
||||||
|
'params',
|
||||||
]
|
]
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -94,6 +95,31 @@ class CategoryTest(TestCase):
|
|||||||
|
|
||||||
self.assertEqual(self.electronics.item_count, self.electronics.partcount())
|
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):
|
def test_invalid_name(self):
|
||||||
# Test that an illegal character is prohibited in a category name
|
# 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'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
|
||||||
url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'),
|
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 = [
|
part_bom_urls = [
|
||||||
|
@ -1872,10 +1872,51 @@ class PartParameterDelete(AjaxDeleteView):
|
|||||||
|
|
||||||
class CategoryDetail(DetailView):
|
class CategoryDetail(DetailView):
|
||||||
""" Detail view for PartCategory """
|
""" Detail view for PartCategory """
|
||||||
|
|
||||||
model = PartCategory
|
model = PartCategory
|
||||||
context_object_name = 'category'
|
context_object_name = 'category'
|
||||||
queryset = PartCategory.objects.all().prefetch_related('children')
|
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):
|
class CategoryEdit(AjaxUpdateView):
|
||||||
|
@ -39,6 +39,7 @@
|
|||||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.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-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="{% static 'css/inventree.css' %}">
|
||||||
<link rel="stylesheet" href="{% get_color_theme_css user.get_username %}">
|
<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-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-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-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/select2/select2.js' %}"></script>
|
||||||
<script type='text/javascript' src="{% static 'script/moment.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={}) {
|
function loadPartTable(table, url, options={}) {
|
||||||
/* Load part listing data into specified table.
|
/* Load part listing data into specified table.
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user