mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 12:06:44 +00:00
Merge pull request #1493 from SchrodingersGat/multi-location-print
Split "part category" view into separate pages
This commit is contained in:
commit
9addad9b46
@ -60,27 +60,42 @@ class CategoryList(generics.ListCreateAPIView):
|
|||||||
queryset = PartCategory.objects.all()
|
queryset = PartCategory.objects.all()
|
||||||
serializer_class = part_serializers.CategorySerializer
|
serializer_class = part_serializers.CategorySerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def filter_queryset(self, queryset):
|
||||||
"""
|
"""
|
||||||
Custom filtering:
|
Custom filtering:
|
||||||
- Allow filtering by "null" parent to retrieve top-level part categories
|
- Allow filtering by "null" parent to retrieve top-level part categories
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cat_id = self.request.query_params.get('parent', None)
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
queryset = super().get_queryset()
|
params = self.request.query_params
|
||||||
|
|
||||||
if cat_id is not None:
|
cat_id = params.get('parent', None)
|
||||||
|
|
||||||
|
cascade = str2bool(params.get('cascade', False))
|
||||||
|
|
||||||
|
# Do not filter by category
|
||||||
|
if cat_id is None:
|
||||||
|
pass
|
||||||
# Look for top-level categories
|
# Look for top-level categories
|
||||||
if isNull(cat_id):
|
elif isNull(cat_id):
|
||||||
|
|
||||||
|
if not cascade:
|
||||||
queryset = queryset.filter(parent=None)
|
queryset = queryset.filter(parent=None)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
cat_id = int(cat_id)
|
category = PartCategory.objects.get(pk=cat_id)
|
||||||
queryset = queryset.filter(parent=cat_id)
|
|
||||||
except ValueError:
|
if cascade:
|
||||||
|
parents = category.get_descendants(include_self=True)
|
||||||
|
parent_ids = [p.id for p in parents]
|
||||||
|
|
||||||
|
queryset = queryset.filter(parent__in=parent_ids)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(parent=category)
|
||||||
|
|
||||||
|
except (ValueError, PartCategory.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include 'part/category_navbar.html' with tab='parts' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class='panel panel-default panel-inventree'>
|
<div class='panel panel-default panel-inventree'>
|
||||||
@ -100,14 +104,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if category and category.children.all|length > 0 %}
|
|
||||||
{% include "part/subcategories.html" with children=category.children.all collapse_id="categories" %}
|
|
||||||
{% elif children|length > 0 %}
|
|
||||||
{% include "part/subcategories.html" with children=children collapse_id="categories" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% block category_content %}
|
||||||
|
|
||||||
<div id='button-toolbar'>
|
<div id='button-toolbar'>
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>
|
<button class='btn btn-default' id='part-export' title='{% trans "Export Part Data" %}'>
|
||||||
@ -150,6 +150,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block category_tables %}
|
{% block category_tables %}
|
||||||
{% endblock category_tables %}
|
{% endblock category_tables %}
|
||||||
@ -162,24 +164,10 @@
|
|||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
{% if category %}
|
|
||||||
enableNavbar({
|
enableNavbar({
|
||||||
label: 'category',
|
label: 'category',
|
||||||
toggleId: '#category-menu-toggle',
|
toggleId: '#category-menu-toggle',
|
||||||
});
|
});
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
if (inventreeLoadInt("show-part-cats") == 1) {
|
|
||||||
$("#collapse-item-categories").collapse('show');
|
|
||||||
}
|
|
||||||
|
|
||||||
$("#collapse-item-categories").on('shown.bs.collapse', function() {
|
|
||||||
inventreeSave('show-part-cats', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#collapse-item-categories").on('hidden.bs.collapse', function() {
|
|
||||||
inventreeDel('show-part-cats');
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#cat-create").click(function() {
|
$("#cat-create").click(function() {
|
||||||
launchModalForm(
|
launchModalForm(
|
||||||
|
@ -8,17 +8,34 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item {% if tab == "subcategories" %}active{% endif %}' title='{% trans "Subcategories" %}'>
|
||||||
|
{% if category %}
|
||||||
|
<a href='{% url "category-subcategory" category.id %}'>
|
||||||
|
{% else %}
|
||||||
|
<a href='{% url "category-index-subcategory" %}'>
|
||||||
|
{% endif %}
|
||||||
|
<span class='fas fa-sitemap'></span>
|
||||||
|
{% trans "Subcategories" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Parts" %}'>
|
<li class='list-group-item {% if tab == "parts" %}active{% endif %}' title='{% trans "Parts" %}'>
|
||||||
|
{% if category %}
|
||||||
<a href='{% url "category-detail" category.id %}'>
|
<a href='{% url "category-detail" category.id %}'>
|
||||||
|
{% else %}
|
||||||
|
<a href='{% url "part-index" %}'>
|
||||||
|
{% endif %}
|
||||||
<span class='fas fa-shapes'></span>
|
<span class='fas fa-shapes'></span>
|
||||||
{% trans "Parts" %}
|
{% trans "Parts" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
{% if category %}
|
||||||
<li class='list-group-item {% if tab == "parameters" %}active{% endif %}' title='{% trans "Parameters" %}'>
|
<li class='list-group-item {% if tab == "parameters" %}active{% endif %}' title='{% trans "Parameters" %}'>
|
||||||
<a href='{% url "category-parametric" category.id %}'>
|
<a href='{% url "category-parametric" category.id %}'>
|
||||||
<span class='fas fa-tasks'></span>
|
<span class='fas fa-tasks'></span>
|
||||||
{% trans "Parameters" %}
|
{% trans "Parameters" %}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
@ -1,22 +0,0 @@
|
|||||||
{% extends "collapse.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% block collapse_title %}
|
|
||||||
{{ children | length }} {% trans 'Child Categories' %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block collapse_content %}
|
|
||||||
<ul class="list-group">
|
|
||||||
{% for child in children %}
|
|
||||||
<li class="list-group-item">
|
|
||||||
<strong><a href="{% url 'category-detail' child.id %}">{{ child.name }}</a></strong>
|
|
||||||
{% if child.description %}
|
|
||||||
<em> - {{ child.description }}</em>
|
|
||||||
{% endif %}
|
|
||||||
{% if child.partcount > 0 %}
|
|
||||||
<span class='badge'>{{ child.partcount }} {% trans 'Part' %}{% if child.partcount > 1 %}s{% endif %}</span>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
51
InvenTree/part/templates/part/subcategory.html
Normal file
51
InvenTree/part/templates/part/subcategory.html
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
{% extends "part/category.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include 'part/category_navbar.html' with tab='subcategories' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block category_content %}
|
||||||
|
|
||||||
|
<div class='panel panel-default panel-inventree'>
|
||||||
|
|
||||||
|
<div class='panel-heading'>
|
||||||
|
<h4>{% trans "Subcategories" %}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id='button-toolbar'>
|
||||||
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
|
|
||||||
|
<div class='filter-list' id='filter-list-category'>
|
||||||
|
<!-- An empty div in which the filter list will be constructed -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' id='subcategory-table' data-toolbar='#button-toolbar'></table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
enableNavbar({
|
||||||
|
label: 'category',
|
||||||
|
toggleId: '#category-menu-toggle',
|
||||||
|
});
|
||||||
|
|
||||||
|
loadPartCategoryTable($('#subcategory-table'), {
|
||||||
|
params: {
|
||||||
|
{% if category %}
|
||||||
|
parent: {{ category.pk }}
|
||||||
|
{% else %}
|
||||||
|
parent: 'null'
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -37,12 +37,54 @@ class PartAPITest(InvenTreeAPITestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def test_get_categories(self):
|
def test_get_categories(self):
|
||||||
""" Test that we can retrieve list of part categories """
|
"""
|
||||||
|
Test that we can retrieve list of part categories,
|
||||||
|
with various filtering options.
|
||||||
|
"""
|
||||||
|
|
||||||
url = reverse('api-part-category-list')
|
url = reverse('api-part-category-list')
|
||||||
|
|
||||||
|
# Request *all* part categories
|
||||||
response = self.client.get(url, format='json')
|
response = self.client.get(url, format='json')
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertEqual(len(response.data), 8)
|
self.assertEqual(len(response.data), 8)
|
||||||
|
|
||||||
|
# Request top-level part categories only
|
||||||
|
response = self.client.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'parent': 'null',
|
||||||
|
},
|
||||||
|
format='json'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 2)
|
||||||
|
|
||||||
|
# Children of PartCategory<1>, cascade
|
||||||
|
response = self.client.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'parent': 1,
|
||||||
|
'cascade': 'true',
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 5)
|
||||||
|
|
||||||
|
# Children of PartCategory<1>, do not cascade
|
||||||
|
response = self.client.get(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
'parent': 1,
|
||||||
|
'cascade': 'false',
|
||||||
|
},
|
||||||
|
format='json',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(response.data), 3)
|
||||||
|
|
||||||
def test_add_categories(self):
|
def test_add_categories(self):
|
||||||
""" Check that we can add categories """
|
""" Check that we can add categories """
|
||||||
data = {
|
data = {
|
||||||
|
@ -88,14 +88,26 @@ category_parameter_urls = [
|
|||||||
url(r'^(?P<pid>\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'),
|
url(r'^(?P<pid>\d+)/delete/', views.CategoryParameterTemplateDelete.as_view(), name='category-param-template-delete'),
|
||||||
]
|
]
|
||||||
|
|
||||||
part_category_urls = [
|
category_urls = [
|
||||||
url(r'^edit/?', views.CategoryEdit.as_view(), name='category-edit'),
|
|
||||||
url(r'^delete/?', views.CategoryDelete.as_view(), name='category-delete'),
|
|
||||||
|
|
||||||
|
# Create a new category
|
||||||
|
url(r'^new/', views.CategoryCreate.as_view(), name='category-create'),
|
||||||
|
|
||||||
|
# Top level subcategory display
|
||||||
|
url(r'^subcategory/', views.PartIndex.as_view(template_name='part/subcategory.html'), name='category-index-subcategory'),
|
||||||
|
|
||||||
|
# Category detail views
|
||||||
|
url(r'(?P<pk>\d+)/', include([
|
||||||
|
url(r'^edit/', views.CategoryEdit.as_view(), name='category-edit'),
|
||||||
|
url(r'^delete/', views.CategoryDelete.as_view(), name='category-delete'),
|
||||||
url(r'^parameters/', include(category_parameter_urls)),
|
url(r'^parameters/', include(category_parameter_urls)),
|
||||||
|
|
||||||
url(r'^parametric/?', views.CategoryParametric.as_view(), name='category-parametric'),
|
url(r'^subcategory/', views.CategoryDetail.as_view(template_name='part/subcategory.html'), name='category-subcategory'),
|
||||||
|
url(r'^parametric/', views.CategoryParametric.as_view(), name='category-parametric'),
|
||||||
|
|
||||||
|
# Anything else
|
||||||
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
url(r'^.*$', views.CategoryDetail.as_view(), name='category-detail'),
|
||||||
|
]))
|
||||||
]
|
]
|
||||||
|
|
||||||
part_bom_urls = [
|
part_bom_urls = [
|
||||||
@ -106,9 +118,6 @@ part_bom_urls = [
|
|||||||
# URL list for part web interface
|
# URL list for part web interface
|
||||||
part_urls = [
|
part_urls = [
|
||||||
|
|
||||||
# Create a new category
|
|
||||||
url(r'^category/new/?', views.CategoryCreate.as_view(), name='category-create'),
|
|
||||||
|
|
||||||
# Create a new part
|
# Create a new part
|
||||||
url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
|
url(r'^new/?', views.PartCreate.as_view(), name='part-create'),
|
||||||
|
|
||||||
@ -125,7 +134,7 @@ part_urls = [
|
|||||||
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
url(r'^(?P<pk>\d+)/', include(part_detail_urls)),
|
||||||
|
|
||||||
# Part category
|
# Part category
|
||||||
url(r'^category/(?P<pk>\d+)/', include(part_category_urls)),
|
url(r'^category/', include(category_urls)),
|
||||||
|
|
||||||
# Part related
|
# Part related
|
||||||
url(r'^related-parts/', include(part_related_urls)),
|
url(r'^related-parts/', include(part_related_urls)),
|
||||||
|
@ -281,27 +281,45 @@ class StockLocationList(generics.ListCreateAPIView):
|
|||||||
queryset = StockLocation.objects.all()
|
queryset = StockLocation.objects.all()
|
||||||
serializer_class = LocationSerializer
|
serializer_class = LocationSerializer
|
||||||
|
|
||||||
def get_queryset(self):
|
def filter_queryset(self, queryset):
|
||||||
"""
|
"""
|
||||||
Custom filtering:
|
Custom filtering:
|
||||||
- Allow filtering by "null" parent to retrieve top-level stock locations
|
- Allow filtering by "null" parent to retrieve top-level stock locations
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = super().get_queryset()
|
queryset = super().filter_queryset(queryset)
|
||||||
|
|
||||||
loc_id = self.request.query_params.get('parent', None)
|
params = self.request.query_params
|
||||||
|
|
||||||
if loc_id is not None:
|
loc_id = params.get('parent', None)
|
||||||
|
|
||||||
|
cascade = str2bool(params.get('cascade', False))
|
||||||
|
|
||||||
|
# Do not filter by location
|
||||||
|
if loc_id is None:
|
||||||
|
pass
|
||||||
# Look for top-level locations
|
# Look for top-level locations
|
||||||
if isNull(loc_id):
|
elif isNull(loc_id):
|
||||||
|
|
||||||
|
# If we allow "cascade" at the top-level, this essentially means *all* locations
|
||||||
|
if not cascade:
|
||||||
queryset = queryset.filter(parent=None)
|
queryset = queryset.filter(parent=None)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
loc_id = int(loc_id)
|
location = StockLocation.objects.get(pk=loc_id)
|
||||||
queryset = queryset.filter(parent=loc_id)
|
|
||||||
except ValueError:
|
# All sub-locations to be returned too?
|
||||||
|
if cascade:
|
||||||
|
parents = location.get_descendants(include_self=True)
|
||||||
|
parent_ids = [p.id for p in parents]
|
||||||
|
queryset = queryset.filter(parent__in=parent_ids)
|
||||||
|
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(parent=location)
|
||||||
|
|
||||||
|
except (ValueError, StockLocation.DoesNotExist):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
@ -320,6 +338,11 @@ class StockLocationList(generics.ListCreateAPIView):
|
|||||||
'description',
|
'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
ordering_fields = [
|
||||||
|
'name',
|
||||||
|
'items',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class StockList(generics.ListCreateAPIView):
|
class StockList(generics.ListCreateAPIView):
|
||||||
""" API endpoint for list view of Stock objects
|
""" API endpoint for list view of Stock objects
|
||||||
|
@ -2,8 +2,15 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% load inventree_extras %}
|
{% load inventree_extras %}
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include "stock/location_navbar.html" with tab="stock" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
|
<div class='panel panel-default panel-inventree'>
|
||||||
|
|
||||||
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
{% setting_object 'STOCK_OWNERSHIP_CONTROL' as owner_control %}
|
||||||
{% if owner_control.value == "True" %}
|
{% if owner_control.value == "True" %}
|
||||||
{% authorized_owners location.owner as owners %}
|
{% authorized_owners location.owner as owners %}
|
||||||
@ -120,36 +127,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if location and location.children.all|length > 0 %}
|
{% block location_content %}
|
||||||
{% include 'stock/location_list.html' with children=location.children.all collapse_id="locations" %}
|
|
||||||
{% elif locations|length > 0 %}
|
|
||||||
{% include 'stock/location_list.html' with children=locations collapse_id="locations" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<hr>
|
<div class='panel panel-default panel-inventree'>
|
||||||
|
<div class='panel-heading'>
|
||||||
{% include "stock_table.html" %}
|
<h4>{% trans "Stock Items" %}</h4>
|
||||||
|
</div>
|
||||||
|
{% include "stock_table.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_load %}
|
</div>
|
||||||
{{ block.super }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js_ready %}
|
{% block js_ready %}
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
|
|
||||||
if (inventreeLoadInt("show-part-locs") == 1) {
|
enableNavbar({
|
||||||
$("#collapse-item-locations").collapse('show');
|
label: 'location',
|
||||||
}
|
toggleId: '#location-menu-toggle'
|
||||||
|
|
||||||
$("#collapse-item-locations").on('shown.bs.collapse', function() {
|
|
||||||
inventreeSave('show-part-locs', 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#collapse-item-locations").on('hidden.bs.collapse', function() {
|
|
||||||
inventreeDel('show-part-locs');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
{% if location %}
|
{% if location %}
|
||||||
@ -261,7 +261,7 @@
|
|||||||
],
|
],
|
||||||
params: {
|
params: {
|
||||||
{% if location %}
|
{% if location %}
|
||||||
location: {{ location.id }},
|
location: {{ location.pk }},
|
||||||
{% endif %}
|
{% endif %}
|
||||||
part_detail: true,
|
part_detail: true,
|
||||||
location_detail: true,
|
location_detail: true,
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
{% extends "collapse.html" %}
|
|
||||||
{% load i18n %}
|
|
||||||
|
|
||||||
{% if roles.stock_location.view or roles.stock.view %}
|
|
||||||
{% block collapse_title %}
|
|
||||||
{% trans 'Sub-Locations' %}<span class='badge'>{{ children|length }}</span>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block collapse_content %}
|
|
||||||
<ul class="list-group">
|
|
||||||
{% for child in children %}
|
|
||||||
<li class="list-group-item"><a href="{% url 'stock-location-detail' child.id %}">{{ child.name }}</a> - <i>{{ child.description }}</i>
|
|
||||||
{% if child.item_count > 0 %}
|
|
||||||
<!-- span class='badge'>{{ child.item_count }} Item{% if child.item_count > 1 %}s{% endif %}</span> -->
|
|
||||||
<span class='badge'>
|
|
||||||
{% comment %}Translators: pluralize with counter{% endcomment %}
|
|
||||||
{% blocktrans count counter=child.item_count %}{{ counter }} Item{% plural %}{{ counter }} Items{% endblocktrans %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
||||||
{% endif %}
|
|
33
InvenTree/stock/templates/stock/location_navbar.html
Normal file
33
InvenTree/stock/templates/stock/location_navbar.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
<ul class='list-group'>
|
||||||
|
|
||||||
|
<li class='list-group-item'>
|
||||||
|
<a href='#' id='location-menu-toggle'>
|
||||||
|
<span class='menu-tab-icon fas fa-expand-arrows-alt'></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item {% if tab == "sublocations" %}active{% endif %}' title='{% trans "Sublocations" %}'>
|
||||||
|
{% if location %}
|
||||||
|
<a href='{% url "stock-location-sublocation" location.id %}'>
|
||||||
|
{% else %}
|
||||||
|
<a href='{% url "stock-sublocations" %}'>
|
||||||
|
{% endif %}
|
||||||
|
<span class='fas fa-sitemap'></span>
|
||||||
|
{% trans "Sublocations" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class='list-group-item {% if tab == "stock" %}active{% endif %}' title='{% trans "Stock Items" %}'>
|
||||||
|
{% if location %}
|
||||||
|
<a href='{% url "stock-location-detail" location.id %}'>
|
||||||
|
{% else %}
|
||||||
|
<a href='{% url "stock-index" %}'>
|
||||||
|
{% endif %}
|
||||||
|
<span class='fas fa-boxes'></span>
|
||||||
|
{% trans "Stock Items" %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
74
InvenTree/stock/templates/stock/sublocation.html
Normal file
74
InvenTree/stock/templates/stock/sublocation.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{% extends "stock/location.html" %}
|
||||||
|
|
||||||
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
|
{% block menubar %}
|
||||||
|
{% include "stock/location_navbar.html" with tab="sublocations" %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block location_content %}
|
||||||
|
|
||||||
|
<div class='panel panel-default panel-inventree'>
|
||||||
|
<div class='panel-heading'>
|
||||||
|
<h4>{% trans "Sublocations" %}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id='button-toolbar'>
|
||||||
|
<div class='button-toolbar container-fluid' style='float: right;'>
|
||||||
|
<!-- Printing actions menu -->
|
||||||
|
<div class='btn-group'>
|
||||||
|
<button id='location-print-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown" title='{% trans "Printing Actions" %}'>
|
||||||
|
<span class='fas fa-print'></span> <span class='caret'></span>
|
||||||
|
</button>
|
||||||
|
<ul class='dropdown-menu'>
|
||||||
|
<li><a href='#' id='multi-location-print-label' title='{% trans "Print labels" %}'><span class='fas fa-tags'></span> {% trans "Print labels" %}</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class='filter-list' id='filter-list-location'>
|
||||||
|
<!-- An empty div in which the filter list will be constructed -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class='table table-striped table-condensed' data-toolbar='#button-toolbar' id='sublocation-table'></table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js_ready %}
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
loadStockLocationTable($('#sublocation-table'), {
|
||||||
|
params: {
|
||||||
|
{% if location %}
|
||||||
|
parent: {{ location.pk }},
|
||||||
|
{% else %}
|
||||||
|
parent: 'null',
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
linkButtonsToSelection(
|
||||||
|
$('#sublocation-table'),
|
||||||
|
[
|
||||||
|
'#location-print-options',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$('#multi-location-print-label').click(function() {
|
||||||
|
|
||||||
|
var selections = $('#sublocation-table').bootstrapTable('getSelections');
|
||||||
|
|
||||||
|
var locations = [];
|
||||||
|
|
||||||
|
selections.forEach(function(loc) {
|
||||||
|
locations.push(loc.pk);
|
||||||
|
});
|
||||||
|
|
||||||
|
printStockLocationLabels(locations);
|
||||||
|
})
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -6,14 +6,21 @@ from django.conf.urls import url, include
|
|||||||
|
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
# URL list for web interface
|
location_urls = [
|
||||||
stock_location_detail_urls = [
|
|
||||||
|
url(r'^new/', views.StockLocationCreate.as_view(), name='stock-location-create'),
|
||||||
|
|
||||||
|
url(r'^(?P<pk>\d+)/', include([
|
||||||
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
|
url(r'^edit/?', views.StockLocationEdit.as_view(), name='stock-location-edit'),
|
||||||
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'),
|
url(r'^delete/?', views.StockLocationDelete.as_view(), name='stock-location-delete'),
|
||||||
url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'),
|
url(r'^qr_code/?', views.StockLocationQRCode.as_view(), name='stock-location-qr'),
|
||||||
|
|
||||||
|
url(r'sublocation/', views.StockLocationDetail.as_view(template_name='stock/sublocation.html'), name='stock-location-sublocation'),
|
||||||
|
|
||||||
# Anything else
|
# Anything else
|
||||||
url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'),
|
url('^.*$', views.StockLocationDetail.as_view(), name='stock-location-detail'),
|
||||||
|
])),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
stock_item_detail_urls = [
|
stock_item_detail_urls = [
|
||||||
@ -49,9 +56,7 @@ stock_tracking_urls = [
|
|||||||
|
|
||||||
stock_urls = [
|
stock_urls = [
|
||||||
# Stock location
|
# Stock location
|
||||||
url(r'^location/(?P<pk>\d+)/', include(stock_location_detail_urls)),
|
url(r'^location/', include(location_urls)),
|
||||||
|
|
||||||
url(r'^location/new/', views.StockLocationCreate.as_view(), name='stock-location-create'),
|
|
||||||
|
|
||||||
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
|
url(r'^item/new/?', views.StockItemCreate.as_view(), name='stock-item-create'),
|
||||||
|
|
||||||
@ -81,5 +86,7 @@ stock_urls = [
|
|||||||
# Individual stock items
|
# Individual stock items
|
||||||
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
|
url(r'^item/(?P<pk>\d+)/', include(stock_item_detail_urls)),
|
||||||
|
|
||||||
|
url(r'^sublocations/', views.StockIndex.as_view(template_name='stock/sublocation.html'), name='stock-sublocations'),
|
||||||
|
|
||||||
url(r'^.*$', views.StockIndex.as_view(), name='stock-index'),
|
url(r'^.*$', views.StockIndex.as_view(), name='stock-index'),
|
||||||
]
|
]
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
{% load inventree_extras %}
|
||||||
|
|
||||||
/* Part API functions
|
/* Part API functions
|
||||||
* Requires api.js to be loaded first
|
* Requires api.js to be loaded first
|
||||||
@ -506,6 +507,82 @@ function loadPartTable(table, url, options={}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function loadPartCategoryTable(table, options) {
|
||||||
|
/* Display a table of part categories */
|
||||||
|
|
||||||
|
var params = options.params || {};
|
||||||
|
|
||||||
|
var filterListElement = options.filterList || '#filter-list-category';
|
||||||
|
|
||||||
|
var filters = {};
|
||||||
|
|
||||||
|
var filterKey = options.filterKey || options.name || 'category';
|
||||||
|
|
||||||
|
if (!options.disableFilters) {
|
||||||
|
filters = loadTableFilters(filterKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
var original = {};
|
||||||
|
|
||||||
|
for (var key in params) {
|
||||||
|
original[key] = params[key];
|
||||||
|
filters[key] = params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFilterList(filterKey, table, filterListElement);
|
||||||
|
|
||||||
|
table.inventreeTable({
|
||||||
|
method: 'get',
|
||||||
|
url: options.url || '{% url "api-part-category-list" %}',
|
||||||
|
queryParams: filters,
|
||||||
|
sidePagination: 'server',
|
||||||
|
name: 'category',
|
||||||
|
original: original,
|
||||||
|
showColumns: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
checkbox: true,
|
||||||
|
title: '{% trans "Select" %}',
|
||||||
|
searchable: false,
|
||||||
|
switchable: false,
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '{% trans "Name" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return renderLink(
|
||||||
|
value,
|
||||||
|
`/part/category/${row.pk}/`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
title: '{% trans "Description" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'pathstring',
|
||||||
|
title: '{% trans "Path" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'parts',
|
||||||
|
title: '{% trans "Parts" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: false,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function yesNoLabel(value) {
|
function yesNoLabel(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
return `<span class='label label-green'>{% trans "YES" %}</span>`;
|
return `<span class='label label-green'>{% trans "YES" %}</span>`;
|
||||||
|
@ -897,6 +897,83 @@ function loadStockTable(table, options) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadStockLocationTable(table, options) {
|
||||||
|
/* Display a table of stock locations */
|
||||||
|
|
||||||
|
var params = options.params || {};
|
||||||
|
|
||||||
|
var filterListElement = options.filterList || '#filter-list-location';
|
||||||
|
|
||||||
|
var filters = {};
|
||||||
|
|
||||||
|
var filterKey = options.filterKey || options.name || 'location';
|
||||||
|
|
||||||
|
if (!options.disableFilters) {
|
||||||
|
filters = loadTableFilters(filterKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
var original = {};
|
||||||
|
|
||||||
|
for (var key in params) {
|
||||||
|
original[key] = params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
setupFilterList(filterKey, table, filterListElement);
|
||||||
|
|
||||||
|
for (var key in params) {
|
||||||
|
filters[key] = params[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
table.inventreeTable({
|
||||||
|
method: 'get',
|
||||||
|
url: options.url || '{% url "api-location-list" %}',
|
||||||
|
queryParams: filters,
|
||||||
|
sidePagination: 'server',
|
||||||
|
name: 'location',
|
||||||
|
original: original,
|
||||||
|
showColumns: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
checkbox: true,
|
||||||
|
title: '{% trans "Select" %}',
|
||||||
|
searchable: false,
|
||||||
|
switchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'name',
|
||||||
|
title: '{% trans "Name" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: true,
|
||||||
|
formatter: function(value, row) {
|
||||||
|
return renderLink(
|
||||||
|
value,
|
||||||
|
`/stock/location/${row.pk}/`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'description',
|
||||||
|
title: '{% trans "Description" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'pathstring',
|
||||||
|
title: '{% trans "Path" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'items',
|
||||||
|
title: '{% trans "Stock Items" %}',
|
||||||
|
switchable: true,
|
||||||
|
sortable: false,
|
||||||
|
sortName: 'item_count',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadStockTrackingTable(table, options) {
|
function loadStockTrackingTable(table, options) {
|
||||||
|
|
||||||
var cols = [
|
var cols = [
|
||||||
|
@ -62,6 +62,28 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filters for "stock location" table
|
||||||
|
if (tableKey == "location") {
|
||||||
|
return {
|
||||||
|
cascade: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Include sublocations" %}',
|
||||||
|
description: '{% trans "Include locations" %}',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters for "part category" table
|
||||||
|
if (tableKey == "category") {
|
||||||
|
return {
|
||||||
|
cascade: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "Include subcategories" %}',
|
||||||
|
description: '{% trans "Include subcategories" %}',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Filters for the "customer stock" table (really a subset of "stock")
|
// Filters for the "customer stock" table (really a subset of "stock")
|
||||||
if (tableKey == "customerstock") {
|
if (tableKey == "customerstock") {
|
||||||
return {
|
return {
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<!-- Printing actions menu -->
|
||||||
<div class='btn-group'>
|
<div class='btn-group'>
|
||||||
<button id='stock-print-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown" title='{% trans "Printing Actions" %}'>
|
<button id='stock-print-options' class='btn btn-primary dropdown-toggle' type='button' data-toggle="dropdown" title='{% trans "Printing Actions" %}'>
|
||||||
<span class='fas fa-print'></span> <span class='caret'></span>
|
<span class='fas fa-print'></span> <span class='caret'></span>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user