Table custom buttons (#5075)

* Add generic implementation for barcode actions

- Commonize code against tables
- Cleaner UI
- Better code
- Will make future react refactor easier

* Add permissions.js

- Separate .js file for dynamically checking permissions

* Update stock table to use client-side actions

* API endpoint for bulk category adjustment

* Bug fix for purchase_order.js

- Prevent some really strange API calls

* Refactor actions for part table

- Now done dynamically

* Refactor actions for the attachment tables

* Refactor actions for build output table

* Increment API version

* Cleanup janky button

* Refactor supplier part table

* Refactor manufacturer part table

* Remove linkButtonsToSelection

- no longer needed
- Cleanup, yay!

* Cleanup purchase order line table

* Refactor BOM table buttons

* JS linting

* Further cleanup

* Template cleanup

- remove extra div elements

* js linting

* js fix
This commit is contained in:
Oliver 2023-06-20 07:45:35 +10:00 committed by GitHub
parent 13389845b1
commit 4c9d4add2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 837 additions and 905 deletions

View File

@ -2,11 +2,14 @@
# InvenTree API version
INVENTREE_API_VERSION = 125
INVENTREE_API_VERSION = 126
"""
Increment this API version number whenever there is a significant change to the API that any clients need to know about
v126 -> 2023-06-19 : https://github.com/inventree/InvenTree/pull/5075
- Adds API endpoint for setting the "category" for multiple parts simultaneously
v125 -> 2023-06-17 : https://github.com/inventree/InvenTree/pull/5064
- Adds API endpoint for setting the "status" field for multiple stock items simultaneously

View File

@ -269,10 +269,6 @@ main {
}
/* Styles for table buttons and filtering */
.button-toolbar .btn {
margin-left: 1px;
margin-right: 1px;
}
.filter-list {
display: inline-block;

View File

@ -95,6 +95,7 @@ notifications_urls = [
dynamic_javascript_urls = [
re_path(r'^calendar.js', DynamicJsView.as_view(template_name='js/dynamic/calendar.js'), name='calendar.js'),
re_path(r'^nav.js', DynamicJsView.as_view(template_name='js/dynamic/nav.js'), name='nav.js'),
re_path(r'^permissions.js', DynamicJsView.as_view(template_name='js/dynamic/permissions.js'), name='permissions.js'),
re_path(r'^settings.js', DynamicJsView.as_view(template_name='js/dynamic/settings.js'), name='settings.js'),
]

View File

@ -165,9 +165,7 @@
</div>
<div class='panel-content'>
<div id='child-button-toolbar'>
<div class='button-toolbar container-fluid float-right'>
{% include "filter_list.html" with id='sub-build' %}
</div>
{% include "filter_list.html" with id='sub-build' %}
</div>
<table class='table table-striped table-condensed' id='sub-build-table' data-toolbar='#child-button-toolbar'></table>
</div>
@ -199,26 +197,8 @@
</div>
</div>
<div class='panel-content'>
{% if build.active %}
{% if build.is_fully_allocated %}
<div class='alert alert-block alert-success'>
{% trans "Untracked stock has been fully allocated for this Build Order" %}
</div>
{% else %}
<div class='alert alert-block alert-danger'>
{% trans "Untracked stock has not been fully allocated for this Build Order" %}
</div>
{% endif %}
{% endif %}
<div id='build-lines-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group'>
<button id='allocate-selected-items' class='btn btn-success' title='{% trans "Allocate selected items" %}'>
<span class='fas fa-sign-in-alt'></span>
</button>
{% include "filter_list.html" with id='buildlines' %}
</div>
</div>
{% include "filter_list.html" with id='buildlines' %}
</div>
<table class='table table-striped table-condensed' id='build-lines-table' data-toolbar='#build-lines-toolbar'></table>
</div>
@ -240,37 +220,7 @@
</div>
<div class='panel-content'>
<div id='build-output-toolbar'>
<div class='button-toolbar container-fluid'>
{% if build.active %}
<div class='btn-group'>
<!-- Build output actions -->
<div class='btn-group'>
<button id='output-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle='dropdown' title='{% trans "Output Actions" %}'>
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
{% if roles.build.add %}
<li><a class='dropdown-item' href='#' id='multi-output-complete' title='{% trans "Complete selected build outputs" %}'>
<span class='fas fa-check-circle icon-green'></span> {% trans "Complete outputs" %}
</a></li>
{% endif %}
{% if roles.build.change %}
<li><a class='dropdown-item' href='#' id='multi-output-scrap' title='{% trans "Scrap selected build outputs" %}'>
<span class='fas fa-times-circle icon-red'></span> {% trans "Scrap outputs" %}
</a></li>
{% endif %}
{% if roles.build.delete %}
<li><a class='dropdown-item' href='#' id='multi-output-delete' title='{% trans "Delete selected build outputs" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete outputs" %}
</a></li>
{% endif %}
</ul>
</div>
{% include "filter_list.html" with id='incompletebuilditems' %}
</div>
{% endif %}
</div>
{% include "filter_list.html" with id='incompletebuilditems' %}
</div>
<table class='table table-striped table-condensed' id='build-output-table' data-toolbar='#build-output-toolbar'></table>
</div>
@ -501,10 +451,6 @@ $('#btn-unallocate').on('click', function() {
});
});
$('#allocate-selected-items').click(function() {
allocateSelectedLines();
});
$("#btn-allocate").on('click', function() {
allocateSelectedLines();
});

View File

@ -24,11 +24,7 @@
<div class='panel-content'>
<div id='button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="build" %}
</div>
</div>
{% include "filter_list.html" with id="build" %}
</div>
<table class='table table-striped table-condensed' id='build-table' data-toolbar='#button-toolbar'>

View File

@ -26,28 +26,7 @@
<div class='panel-content'>
{% if roles.purchase_order.change %}
<div id='supplier-part-button-toolbar'>
<div class='button-toolbar container-fluid'>
<div class='btn-group' role='group'>
<div class='btn-group'>
<button class="btn btn-primary dropdown-toggle" id='supplier-table-options' type="button" data-bs-toggle="dropdown">
<span class='fas fa-tools'></span> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if roles.purchase_order.add %}
<li><a class='dropdown-item' href='#' id='multi-supplier-part-order' title='{% trans "Order parts" %}'>
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
</a></li>
{% endif %}
{% if roles.purchase_order.delete %}
<li><a class='dropdown-item' href='#' id='multi-supplier-part-delete' title='{% trans "Delete parts" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Parts" %}
</a></li>
{% endif %}
</ul>
</div>
{% include "filter_list.html" with id="supplier-part" %}
</div>
</div>
{% include "filter_list.html" with id="supplier-part" %}
</div>
{% endif %}
@ -73,29 +52,7 @@
<div class='panel-content'>
{% if roles.purchase_order.change %}
<div id='manufacturer-part-button-toolbar'>
<div class='button-toolbar container-fluid'>
<div class='btn-group' role='group'>
<div class='btn-group' role='group'>
<button class="btn btn-primary dropdown-toggle" id='manufacturer-table-options' type="button" data-bs-toggle="dropdown">
<span class='fas fa-tools'></span>
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if roles.purchase_order.add %}
<li><a class='dropdown-item' href='#' id='multi-manufacturer-part-order' title='{% trans "Order parts" %}'>
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
</a></li>
{% endif %}
{% if roles.purchase_order.delete %}
<li><a class='dropdown-item' href='#' id='multi-manufacturer-part-delete' title='{% trans "Delete parts" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Parts" %}
</a></li>
{% endif %}
</ul>
</div>
{% include "filter_list.html" with id="manufacturer-part" %}
</div>
</div>
{% include "filter_list.html" with id="manufacturer-part" %}
</div>
{% endif %}
<table class='table table-striped table-condensed' id='manufacturer-part-table' data-toolbar='#manufacturer-part-button-toolbar'>
@ -128,9 +85,7 @@
</div>
<div class='panel-content'>
<div id='po-button-bar'>
<div class='button-toolbar container-fluid' style='float: right;'>
{% include "filter_list.html" with id="purchaseorder" %}
</div>
{% include "filter_list.html" with id="purchaseorder" %}
</div>
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#po-button-bar'>
@ -156,9 +111,7 @@
</div>
<div class='panel-content'>
<div id='so-button-bar'>
<div class='button-toolbar container-fluid' style='float: right;'>
{% include "filter_list.html" with id="salesorder" %}
</div>
{% include "filter_list.html" with id="salesorder" %}
</div>
<table class='table table-striped table-condensed po-table' id='sales-order-table' data-toolbar='#so-button-bar'>
@ -174,9 +127,7 @@
</div>
<div class='panel-content'>
<div id='assigned-stock-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="customerstock" %}
</div>
{% include "filter_list.html" with id="customerstock" %}
</div>
<table class='table table-striped table-condensed' id='assigned-stock-table' data-toolbar='#assigned-stock-button-toolbar'></table>
@ -201,11 +152,7 @@
</div>
<div class='panel-content'>
<div id='table-buttons'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="returnorder" %}
</div>
</div>
{% include "filter_list.html" with id="returnorder" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#table-buttons' id='return-order-table'>
</table>
@ -246,9 +193,7 @@
</div>
<div class='panel-content'>
<div id='contacts-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="contacts" %}
</div>
{% include "filter_list.html" with id="contacts" %}
</div>
<table class='table table-striped table-condensed' id='contacts-table' data-toolbar='#contacts-button-toolbar'></table>
@ -271,9 +216,7 @@
</div>
<div class='panel-content'>
<div id='addresses-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="addresses" %}
</div>
{% include "filter_list.html" with id="addresses" %}
</div>
<table class='table table-striped table-condensed' id='addresses-table' data-toolbar='#addresses-button-toolbar'></table>
@ -498,32 +441,6 @@
}
);
$("#multi-manufacturer-part-delete").click(function() {
var selections = getTableData('#manufacturer-part-table');
deleteManufacturerParts(selections, {
success: function() {
$("#manufacturer-part-table").bootstrapTable('refresh');
}
});
});
$("#multi-manufacturer-part-order").click(function() {
var selections = getTableData('#manufacturer-part-table');
var parts = [];
selections.forEach(function(item) {
var part = item.part_detail;
part.manufacturer_part = item.pk;
parts.push(part);
});
orderParts(
parts,
);
});
{% endif %}
{% if company.is_supplier %}
@ -552,37 +469,6 @@
},
}
);
$("#multi-supplier-part-delete").click(function() {
var selections = getTableData("#supplier-part-table");
deleteSupplierParts(selections, {
success: function() {
$('#supplier-part-table').bootstrapTable('refresh');
}
});
});
$("#multi-supplier-part-order").click(function() {
var selections = getTableData('#supplier-part-table');
var parts = [];
selections.forEach(function(item) {
var part = item.part_detail;
parts.push(part);
});
orderParts(
parts,
{
supplier: {{ company.pk }},
}
);
});
{% endif %}
enableSidebar('company');

View File

@ -24,11 +24,11 @@
<div class='panel-content'>
<div id='button-toolbar'>
<div id='company-button-toolbar'>
{% include "filter_list.html" with id='company' %}
</div>
<table class='table table-striped table-condensed' id='company-table' data-toolbar='#button-toolbar'>
<table class='table table-striped table-condensed' id='company-table' data-toolbar='#company-button-toolbar'>
</table>
</div>

View File

@ -127,17 +127,7 @@ src="{% static 'img/blank_image.png' %}"
</div>
<div class='panel-content'>
<div id='supplier-button-toolbar'>
<div class='btn-group'>
<div id='opt-dropdown' class="btn-group">
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<span class='fas fa-tools'></span> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a class='dropdown-item' href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
</ul>
</div>
{% include "filter_list.html" with id='supplier-part' %}
</div>
{% include "filter_list.html" with id='supplier-part' %}
</div>
<table class="table table-striped table-condensed" id='supplier-table' data-toolbar='#supplier-button-toolbar'>
@ -174,17 +164,7 @@ src="{% static 'img/blank_image.png' %}"
</div>
<div class='panel-content'>
<div id='parameter-toolbar'>
<div class='btn-group'>
<div id='opt-dropdown' class="btn-group">
<button id='parameter-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">
<span class='fas fa-tools'></span> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a class='dropdown-item' href='#' id='multi-parameter-delete' title='{% trans "Delete parameters" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
</ul>
</div>
{% include "filter_list.html" with id="manufacturer-part-parameters" %}
</div>
{% include "filter_list.html" with id="manufacturer-part-parameters" %}
</div>
<table class='table table-striped table-condensed' id='parameter-table' data-toolbar='#parameter-toolbar'></table>
@ -240,26 +220,6 @@ $('#supplier-create').click(function () {
});
});
$("#supplier-part-delete").click(function() {
var selections = getTableData('#supplier-table');
deleteSupplierParts(selections, {
success: reloadSupplierPartTable,
});
});
$("#multi-parameter-delete").click(function() {
var selections = getTableData('#parameter-table');
deleteManufacturerPartParameters(selections, {
success: function() {
$('#parameter-table').bootstrapTable('refresh');
}
});
});
loadSupplierPartTable(
"#supplier-table",
"{% url 'api-supplier-part-list' %}",

View File

@ -234,9 +234,7 @@ src="{% static 'img/blank_image.png' %}"
</div>
<div class='panel-content'>
<div id='button-bar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id='purchaseorder' %}
</div>
{% include "filter_list.html" with id='purchaseorder' %}
</div>
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#button-bar'>
</table>
@ -258,10 +256,8 @@ src="{% static 'img/blank_image.png' %}"
</div>
</div>
<div class='panel-content'>
<div id='price-break-toolbar' class='btn-group'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id='supplierpricebreak' %}
</div>
<div id='price-break-toolbar'>
{% include "filter_list.html" with id='supplierpricebreak' %}
</div>
<table class='table table-striped table-condensed' id='price-break-table' data-toolbar='#price-break-toolbar'>

View File

@ -37,23 +37,7 @@
</div>
</div>
<div class='panel-content'>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
{% if roles.purchase_order.change %}
{% if order.is_open or allow_extra_editing %}
<div class='btn-group' role='group'>
<!-- Multiple-select actions -->
<button id='multi-select-options' class='btn btn-primary dropdown-toggle' data-bs-toggle='dropdown'>
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li><a class='dropdown-item' href='#' id='po-lines-bulk-delete' title='{% trans "Delete Line Items" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Line Items" %}
</a></li>
</ul>
</div>
{% endif %}
{% endif %}
<div id='order-toolbar-buttons'>
{% include "filter_list.html" with id="purchase-order-lines" %}
</div>
@ -77,10 +61,8 @@
</div>
</div>
<div class='panel-content'>
<div id='order-extra-toolbar-buttons' class='btn-group' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="purchase-order-extra-lines" %}
</div>
<div id='order-extra-toolbar-buttons'>
{% include "filter_list.html" with id="purchase-order-extra-lines" %}
</div>
<table class='table table-striped table-condensed' id='po-extra-lines-table' data-toolbar='#order-extra-toolbar-buttons'>
</table>

View File

@ -24,11 +24,7 @@
<div class='panel-content'>
<div id='table-buttons'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="purchaseorder" %}
</div>
</div>
{% include "filter_list.html" with id="purchaseorder" %}
</div>
<table class='table table-striped table-condensed po-table' data-toolbar='#table-buttons' id='purchase-order-table'>

View File

@ -34,10 +34,8 @@
</div>
</div>
<div class='panel-content'>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="returnorderlines" %}
</div>
<div id='order-toolbar-buttons'>
{% include "filter_list.html" with id="returnorderlines" %}
</div>
<table class='table table-striped table-condensed' id='return-order-lines-table' data-toolbar='#order-toolbar-buttons'>
</table>
@ -58,10 +56,8 @@
</div>
</div>
<div class='panel-content'>
<div id='order-extra-toolbar-buttons' class='btn-group' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="return-order-extra-lines" %}
</div>
<div id='order-extra-toolbar-buttons'>
{% include "filter_list.html" with id="return-order-extra-lines" %}
</div>
<table class='table table-striped table-condensed' id='return-order-extra-lines-table' data-toolbar='#order-extra-toolbar-buttons'>
</table>

View File

@ -26,15 +26,11 @@
{% block page_info %}
<div class='panel-content'>
<div id='table-buttons'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="returnorder" %}
</div>
</div>
<div id='return-order-table-buttons'>
{% include "filter_list.html" with id="returnorder" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#table-buttons' id='return-order-table'>
<table class='table table-striped table-condensed' data-toolbar='#return-order-table-buttons' id='return-order-table'>
</table>
<div id='return-order-calendar'></div>

View File

@ -29,10 +29,8 @@
</div>
</div>
<div class='panel-content'>
<div id='order-toolbar-buttons' class='btn-group' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="sales-order-lines" %}
</div>
<div id='order-toolbar-buttons'>
{% include "filter_list.html" with id="sales-order-lines" %}
</div>
<table class='table table-striped table-condensed' id='so-lines-table' data-toolbar='#order-toolbar-buttons'>
</table>
@ -54,10 +52,8 @@
</div>
</div>
<div class='panel-content'>
<div id='order-extra-toolbar-buttons' class='btn-group' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="sales-order-extra-lines" %}
</div>
<div id='order-extra-toolbar-buttons'>
{% include "filter_list.html" with id="sales-order-extra-lines" %}
</div>
<table class='table table-striped table-condensed' id='so-extra-lines-table' data-toolbar='#order-extra-toolbar-buttons'>
</table>
@ -89,10 +85,8 @@
</div>
<div class='panel-content'>
{% if roles.sales_order.change %}
<div id='pending-shipment-toolbar' class='btn-group' style='float: right;'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="pending-shipments" %}
</div>
<div id='pending-shipment-toolbar'>
{% include "filter_list.html" with id="pending-shipments" %}
</div>
{% endif %}
<table class='table table-striped table-condensed' id='pending-shipments-table' data-toolbar='#pending-shipment-toolbar'></table>
@ -105,10 +99,8 @@
<h4>{% trans "Completed Shipments" %}</h4>
</div>
<div class='panel-content'>
<div id='completed-shipment-toolbar' class='btn-group' style='float: right;'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="completed-shipments" %}
</div>
<div id='completed-shipment-toolbar'>
{% include "filter_list.html" with id="completed-shipments" %}
</div>
<table class='table table-striped table-condensed' id='completed-shipments-table' data-toolbar='#completed-shipment-toolbar'></table>
</div>
@ -119,10 +111,8 @@
<h4>{% trans "Build Orders" %}</h4>
</div>
<div class='panel-content'>
<div id='builds-toolbar' class='btn-group' style='float: right;'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id='build' %}
</div>
<div id='builds-toolbar'>
{% include "filter_list.html" with id='build' %}
</div>
<table class='table table-striped table-condensed' id='builds-table' data-toolbar='#builds-toolbar'></table>
</div>

View File

@ -27,10 +27,8 @@
<div class='panel-content'>
<div id='table-buttons'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group'>
{% include "filter_list.html" with id="salesorder" %}
</div>
<div class='button-toolbar'>
{% include "filter_list.html" with id="salesorder" %}
</div>
</div>

View File

@ -1271,6 +1271,13 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
]
class PartChangeCategory(CreateAPI):
"""API endpoint to change the location of multiple parts in bulk"""
serializer_class = part_serializers.PartSetCategorySerializer
queryset = Part.objects.none()
class PartDetail(PartMixin, RetrieveUpdateDestroyAPI):
"""API endpoint for detail view of a single Part object."""
@ -2020,6 +2027,8 @@ part_api_urls = [
re_path(r'^.*$', PartDetail.as_view(), name='api-part-detail'),
])),
re_path(r'^change_category/', PartChangeCategory.as_view(), name='api-part-change-category'),
re_path(r'^.*$', PartList.as_view(), name='api-part-list'),
]

View File

@ -291,6 +291,56 @@ class PartBriefSerializer(InvenTree.serializers.InvenTreeModelSerializer):
pricing_max = InvenTree.serializers.InvenTreeMoneySerializer(source='pricing_data.overall_max', allow_null=True, read_only=True)
class PartSetCategorySerializer(serializers.Serializer):
"""Serializer for changing PartCategory for multiple Part objects"""
class Meta:
"""Metaclass options"""
fields = [
'parts',
'category',
]
parts = serializers.PrimaryKeyRelatedField(
queryset=Part.objects.all(),
many=True, required=True, allow_null=False,
label=_('Parts'),
)
def validate_parts(self, parts):
"""Validate the selected parts"""
if len(parts) == 0:
raise serializers.ValidationError(_("No parts selected"))
return parts
category = serializers.PrimaryKeyRelatedField(
queryset=PartCategory.objects.filter(structural=False),
many=False, required=True, allow_null=False,
label=_('Category'),
help_text=_('Select category',)
)
@transaction.atomic
def save(self):
"""Save the serializer to change the location of the selected parts"""
data = self.validated_data
parts = data['parts']
category = data['category']
parts_to_save = []
for p in parts:
if p.category == category:
continue
p.category = category
parts_to_save.append(p)
Part.objects.bulk_update(parts_to_save, ['category'])
class DuplicatePartSerializer(serializers.Serializer):
"""Serializer for specifying options when duplicating a Part.

View File

@ -23,20 +23,7 @@
{% endif %}
<div id='bom-button-toolbar'>
<div class="btn-group" role="group" aria-label="...">
{% if roles.part.change %}
<!-- Action menu -->
<div class='btn-group'>
<button id='bom-actions' title='{% trans "BOM actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu' role='menu'>
<li><a class='dropdown-item' href='#' id='bom-item-delete'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Items" %}</a></li>
</ul>
</div>
{% endif %}
{% include "filter_list.html" with id="bom" %}
</div>
{% include "filter_list.html" with id="bom" %}
</div>
<table class='table table-bom table-condensed' data-toolbar="#bom-button-toolbar" id='bom-table'>

View File

@ -169,24 +169,7 @@
</div>
</div>
<div id='part-button-toolbar'>
<div class='btn-group' role='group'>
<div class='btn-group' role='group'>
<button id='part-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle="dropdown">
<span class='fas fa-tools' title='{% trans "Options" %}'></span>
</button>
<ul class='dropdown-menu'>
{% if roles.part.change %}
<li><a class='dropdown-item' href='#' id='multi-part-category' title='{% trans "Set category" %}'>
<span class='fas fa-sitemap'></span> {% trans "Set Category" %}
</a></li>
{% endif %}
<li><a class='dropdown-item' href='#' id='multi-part-order' title='{% trans "Order parts" %}'>
<span class='fas fa-shopping-cart'></span> {% trans "Order Parts" %}
</a></li>
</ul>
</div>
{% include "filter_list.html" with id="parts" %}
</div>
{% include "filter_list.html" with id="parts" %}
</div>
<div class='panel-content'>
<table class='table table-striped table-condensed' data-toolbar='#part-button-toolbar' id='part-table'>
@ -209,9 +192,7 @@
</div>
<div class='panel-content'>
<div id='param-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="parameters" %}
</div>
{% include "filter_list.html" with id="parameters" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#param-button-toolbar' id='parametric-part-table'>
@ -235,9 +216,7 @@
</div>
<div class='panel-content'>
<div id='subcategory-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="category" %}
</div>
{% include "filter_list.html" with id="category" %}
</div>
<table class='table table-striped table-condensed' id='subcategory-table' data-toolbar='#subcategory-button-toolbar'></table>

View File

@ -93,9 +93,7 @@
</div>
<div class='panel-content'>
<div id='test-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="parttests" %}
</div>
{% include "filter_list.html" with id="parttests" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#test-button-toolbar' id='test-template-table'></table>
@ -116,9 +114,7 @@
</div>
<div class='panel-content'>
<div id='po-button-bar'>
<div class='button-toolbar container-fluid' style='float: right;'>
{% include "filter_list.html" with id="partpurchaseorders" %}
</div>
{% include "filter_list.html" with id="partpurchaseorders" %}
</div>
<table class='table table-striped table-condensed po-table' id='purchase-order-table' data-toolbar='#po-button-bar'>
@ -132,9 +128,7 @@
</div>
<div class='panel-content'>
<div id='so-button-bar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="salesorder" %}
</div>
{% include "filter_list.html" with id="salesorder" %}
</div>
<table class='table table-striped table-condensed po-table' id='sales-order-table' data-toolbar='#so-button-bar'>
@ -145,11 +139,8 @@
<h4>{% trans "Sales Order Allocations" %}</h4>
</div>
<div class='panel-content'>
<div id='sales-order-allocation-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="salesorderallocation" %}
</div>
{% include "filter_list.html" with id="salesorderallocation" %}
</div>
<table class='table table-striped table-condensed' id='sales-order-allocation-table' data-toolbar='#sales-order-allocation-button-toolbar'></table>
</div>
@ -190,11 +181,7 @@
</div>
<div class='panel-content'>
<div id='variant-button-toolbar'>
<div class='button-toolbar container-fluid'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="variants" %}
</div>
</div>
{% include "filter_list.html" with id="variants" %}
</div>
<table class='table table-striped table-condensed' id='variants-table' data-toolbar='#variant-button-toolbar'>
@ -218,11 +205,7 @@
</div>
<div class='panel-content'>
<div id='param-button-toolbar'>
<div class='button-toolbar container-fluid'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="parameters" %}
</div>
</div>
{% include "filter_list.html" with id="parameters" %}
</div>
<table id='parameter-table' class='table table-condensed table-striped' data-toolbar="#param-button-toolbar"></table>
</div>
@ -259,9 +242,7 @@
</div>
<div class='panel-content'>
<div id='related-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="related" %}
</div>
{% include "filter_list.html" with id="related" %}
</div>
<table id='related-parts-table' class='table table-striped table-condensed' data-toolbar='#related-button-toolbar'></table>
@ -317,9 +298,7 @@
</div>
<div class='panel-content'>
<div id='assembly-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="usedin" %}
</div>
{% include "filter_list.html" with id="usedin" %}
</div>
<table class="table table-striped table-condensed" id='used-table' data-toolbar='#assembly-button-toolbar'>
@ -346,9 +325,7 @@
</div>
<div class='panel-content'>
<div id='build-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="build" %}
</div>
{% include "filter_list.html" with id="build" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#build-button-toolbar' id='build-table'>
@ -362,9 +339,7 @@
</div>
<div class='panel-content'>
<div id='build-allocation-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="buildorderallocation" %}
</div>
{% include "filter_list.html" with id="buildorderallocation" %}
</div>
<table class='table table-striped table-condensed' id='build-order-allocation-table' data-toolbar='#build-allocation-button-toolbar'></table>
</div>
@ -385,17 +360,7 @@
</div>
<div class='panel-content'>
<div id='supplier-button-toolbar'>
<div class='btn-group'>
<div id='opt-dropdown' class="btn-group">
<button id='supplier-part-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">{% trans "Options" %} <span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a class='dropdown-item' href='#' id='supplier-part-delete' title='{% trans "Delete supplier parts" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}
</a></li>
</ul>
</div>
{% include "filter_list.html" with id="supplier-part" %}
</div>
{% include "filter_list.html" with id="supplier-part" %}
</div>
<table class="table table-striped table-condensed" id='supplier-part-table' data-toolbar='#supplier-button-toolbar'>
@ -416,15 +381,7 @@
<div class='panel-content'>
<div class='panel-content'>
<div id='manufacturer-button-toolbar'>
<div class='btn-group'>
<div id='opt-dropdown' class="btn-group">
<button id='manufacturer-part-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown">{% trans "Options" %} <span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a class='dropdown-item' href='#' id='manufacturer-part-delete' title='{% trans "Delete manufacturer parts" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete" %}</a></li>
</ul>
{% include "filter_list.html" with id="manufacturer-part" %}
</div>
</div>
{% include "filter_list.html" with id="manufacturer-part" %}
</div>
<table class='table table-condensed table-striped' id='manufacturer-part-table' data-toolbar='#manufacturer-button-toolbar'></table>
</div>
@ -526,8 +483,6 @@
}
);
linkButtonsToSelection($("#supplier-part-table"), ['#supplier-part-options']);
loadManufacturerPartTable(
'#manufacturer-part-table',
"{% url 'api-manufacturer-part-list' %}",
@ -540,8 +495,6 @@
}
);
linkButtonsToSelection($("#manufacturer-part-table"), ['#manufacturer-part-options']);
$("#manufacturer-part-delete").click(function() {
var selectionss = getTableData('#manufacturer-part-table');
@ -625,12 +578,6 @@
sub_part_detail: true,
});
linkButtonsToSelection($("#bom-table"),
[
"#bom-item-delete",
]
);
$('#bom-item-delete').click(function() {
// Get a list of the selected BOM items

View File

@ -22,9 +22,7 @@
</div>
<div class='panel-content'>
<div id='tracking-table-toolbar'>
<div class='btn-group'>
{% include "filter_list.html" with id="stocktracking" %}
</div>
{% include "filter_list.html" with id="stocktracking" %}
</div>
<table class='table table-condensed table-striped' id='track-table' data-toolbar='#tracking-table-toolbar'>
</table>
@ -40,9 +38,7 @@
</div>
<div class='panel-content'>
<div id='build-order-allocations-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="buildorderallocation" %}
</div>
{% include "filter_list.html" with id="buildorderallocation" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#build-order-allocations-toolbar' id='build-order-allocation-table'></table>
</div>
@ -55,9 +51,7 @@
</div>
<div class='panel-content'>
<div id='sales-order-allocations-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="salesorderallocation" %}
</div>
{% include "filter_list.html" with id="salesorderallocation" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#sales-order-allocations-toolbar' id='sales-order-allocation-table'></table>
</div>
@ -102,9 +96,7 @@
</div>
<div class='panel-content'>
<div id='test-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="stocktests" %}
</div>
{% include "filter_list.html" with id="stocktests" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#test-button-toolbar' id='test-result-table'></table>
@ -157,9 +149,7 @@
</div>
<div class='panel-content'>
<div id='installed-table-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id='installed-items' %}
</div>
{% include "filter_list.html" with id='installed-items' %}
</div>
<table class='table table-striped table-condensed' id='installed-table' data-toolbar='#installed-table-toolbar'></table>
</div>

View File

@ -221,9 +221,7 @@
</div>
<div class='panel-content'>
<div id='sublocation-button-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="location" %}
</div>
{% include "filter_list.html" with id="location" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#sublocation-button-toolbar' id='sublocation-table'></table>
@ -270,13 +268,6 @@
});
});
linkButtonsToSelection(
$('#sublocation-table'),
[
'#location-print-options',
]
);
{% if labels_enabled %}
$('#print-label').click(function() {

View File

@ -18,9 +18,7 @@
{% block content %}
<div id='history-buttons'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="notifications-history" %}
</div>
{% include "filter_list.html" with id="notifications-history" %}
</div>
<div class='row'>

View File

@ -18,9 +18,7 @@
{% block content %}
<div id='inbox-buttons'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="notifications-inbox" %}
</div>
{% include "filter_list.html" with id="notifications-inbox" %}
</div>
<div class='row'>

View File

@ -35,9 +35,7 @@
<div class='panel-content'>
<div id='part-stocktake-report-toolbar'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="stocktakereport" %}
</div>
{% include "filter_list.html" with id="stocktakereport" %}
</div>
<table class='table table-striped table-condensed' id='stocktake-report-table' data-toolbar='#part-stocktake-report-toolbar'></table>
</div>

View File

@ -51,11 +51,7 @@
{% endif %}
<div id='plugin-button-toolbar'>
<div class='button-toolbar container-fluid'>
<div class='btn-group' role='group'>
{% include "filter_list.html" with id="plugins" %}
</div>
</div>
{% include "filter_list.html" with id="plugins" %}
</div>
<div class='table-responsive'>
<table class='table table-striped table-condensed' id='plugin-table' data-toolbar='#plugin-button-toolbar'></table>

View File

@ -1,21 +1,7 @@
{% load i18n %}
<div id='attachment-buttons'>
<div class='btn-group' role='group'>
<div class='btn-group' id='multi-attachment-actions'>
<button class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle='dropdown' title='{% trans "Actions" %}'>
<span class='fas fa-tools'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li>
<a class='dropdown-item' href='#' id='multi-attachment-delete' title='{% trans "Delete selected attachments" %}'>
<span class='fas fa-trash-alt icon-red'></span> {% trans "Delete Attachments" %}
</a>
</li>
</ul>
</div>
{% include "filter_list.html" with id="attachments" %}
</div>
{% include "filter_list.html" with id="attachments" %}
</div>
<div class='dropzone' id='attachment-dropzone'>

View File

@ -147,6 +147,7 @@
<!-- dynamic javascript templates -->
<script defer type='text/javascript' src="{% url 'calendar.js' %}"></script>
<script defer type='text/javascript' src="{% url 'nav.js' %}"></script>
<script defer type='text/javascript' src="{% url 'permissions.js' %}"></script>
<script defer type='text/javascript' src="{% url 'settings.js' %}"></script>
<!-- translated javascript templates-->

View File

@ -1 +1,3 @@
<div class='filter-list d-flex flex-row form-row' id='filter-list-{% if prefix %}{{ prefix }}{% endif %}{{ id }}'><!-- Empty div for table filters --></div>
<div class='filter-list d-flex flex-row form-row' id='filter-list-{% if prefix %}{{ prefix }}{% endif %}{{ id }}'>
<!-- Empty div for table filters -->
</div>

View File

@ -0,0 +1,59 @@
/*
* globals
inventreeGet,
/* exported
checkPermission,
*/
// Keep track of the current user permissions
var user_roles = null;
/*
* Check if the user has the specified role and permission
*/
function checkPermission(role, permission='view') {
// Allow permission to be specified in dotted notation, e.g. 'part.add'
if (role.indexOf('.') > 0) {
let parts = role.split('.');
role = parts[0];
permission = parts[1];
}
// Request user roles if we do not have them
if (user_roles == null) {
inventreeGet('{% url "api-user-roles" %}', {}, {
async: false,
success: function(response) {
user_roles = response.roles || {};
}
});
}
if (user_roles == null) {
console.error("Failed to fetch user roles");
return false;
}
if (!(role in user_roles)) {
return false;
}
let roles = user_roles[role];
if (!roles) {
return false;
}
let found = false;
user_roles[role].forEach(function(p) {
if (String(p).valueOf() == String(permission).valueOf()) {
found = true;
}
});
return found;
}

View File

@ -187,7 +187,27 @@ function attachmentLink(filename) {
let html = makeIcon(icon) + ` ${fn}`;
return renderLink(html, filename, {download: true});
}
/*
* Construct a set of actions for an attachment table,
* with the provided permission set
*/
function makeAttachmentActions(permissions, options) {
let actions = [];
if (permissions.delete) {
actions.push({
label: 'delete',
icon: 'fa-trash-alt icon-red',
title: '{% trans "Delete attachments" %}',
callback: options.callback,
});
}
return actions;
}
@ -225,7 +245,20 @@ function loadAttachmentTable(url, options) {
}
});
setupFilterList('attachments', $(table), '#filter-list-attachments');
setupFilterList('attachments', $(table), '#filter-list-attachments', {
custom_actions: [
{
label: 'attachments',
icon: 'fa-tools',
title: '{% trans "Attachment actions" %}',
actions: makeAttachmentActions(permissions, {
callback: function(attachments) {
deleteAttachments(attachments, url, options);
}
}),
}
]
});
if (permissions.add) {
addAttachmentButtonCallbacks(url, options.fields || {});
@ -235,19 +268,6 @@ function loadAttachmentTable(url, options) {
$('#new-attachment-link').hide();
}
if (permissions.delete) {
// Add callback for the 'multi delete' button
$('#multi-attachment-delete').click(function() {
var attachments = getTableData(table);
if (attachments.length > 0) {
deleteAttachments(attachments, url, options);
}
});
} else {
$('#multi-attachment-actions').hide();
}
$(table).inventreeTable({
url: url,
name: options.name || 'attachments',
@ -286,16 +306,6 @@ function loadAttachmentTable(url, options) {
});
});
}
if (permissions.delete) {
// Add callback for 'delete' button
$(table).find('.button-attachment-delete').click(function() {
var pk = $(this).attr('pk');
var attachment = $(table).bootstrapTable('getRowByUniqueId', pk);
deleteAttachments([attachment], url, options);
});
}
},
columns: [
{

View File

@ -763,7 +763,7 @@ function scanItemsIntoLocation(item_list, options={}) {
// Extra form fields
var extra = makeNotesField();
// Header contentfor
// Header content
var header = `
<div id='header-div'>
</div>

View File

@ -33,6 +33,7 @@
modalSetContent,
partFields,
partGroups,
reloadBootstrapTable,
renderLink,
setupFilterList,
shortenString,
@ -817,7 +818,24 @@ function loadBomTable(table, options={}) {
Object.assign(filters, params);
setupFilterList('bom', $(table));
setupFilterList('bom', $(table), '#filter-list-bom', {
custom_actions: [{
label: 'actions',
actions: [{
label: 'delete',
title: '{% trans "Delete items" %}',
icon: 'fa-trash-alt icon-red',
permission: 'part.change',
callback: function(data) {
deleteBomItems(data, {
success: function() {
reloadBootstrapTable('#bom-table');
}
});
}
}]
}]
});
function availableQuantity(row) {

View File

@ -23,7 +23,6 @@
inventreeLoad,
inventreePut,
launchModalForm,
linkButtonsToSelection,
loadTableFilters,
locationDetail,
makeDeleteButton,
@ -34,6 +33,7 @@
makePartIcons,
makeProgressBar,
orderParts,
reloadBootstrapTable,
renderDate,
renderLink,
setupFilterList,
@ -387,7 +387,7 @@ function createBuildOutput(build_id, options) {
fields: fields,
preFormContent: html,
onSuccess: function(response) {
location.reload();
reloadBootstrapTable(options.table || '#build-output-table');
},
});
@ -995,6 +995,70 @@ function loadBuildOrderAllocationTable(table, options={}) {
}
/*
* Construct a set of actions for the build output table
*/
function makeBuildOutputActions(build_info) {
return [
{
label: 'complete',
title: '{% trans "Complete outputs" %}',
icon: 'fa-check-circle icon-green',
permission: 'build.add',
callback: function(data) {
completeBuildOutputs(
build_info.pk,
data,
{
success: function() {
$('#build-output-table').bootstrapTable('refresh'); // Reload the "in progress" table
$('#build-stock-table').bootstrapTable('refresh'); // Reload the "completed" table
}
}
);
},
},
{
label: 'scrap',
title: '{% trans "Scrap outputs" %}',
icon: 'fa-times-circle icon-red',
permission: 'build.change',
callback: function(data) {
scrapBuildOutputs(
build_info.pk,
data,
{
success: function() {
$('#build-output-table').bootstrapTable('refresh'); // Reload the "in progress" table
$('#build-stock-table').bootstrapTable('refresh'); // Reload the "completed" table
}
}
);
},
},
{
label: 'delete',
title: '{% trans "Delete outputs" %}',
icon: 'fa-trash-alt icon-red',
permission: 'build.delete',
callback: function(data) {
deleteBuildOutputs(
build_info.pk,
data,
{
success: function() {
$('#build-output-table').bootstrapTable('refresh'); // Reload the "in progress" table
$('#build-stock-table').bootstrapTable('refresh'); // Reload the "completed" table
}
}
)
},
}
];
}
/*
* Display a "build output" table for a particular build.
*
@ -1035,6 +1099,12 @@ function loadBuildOutputTable(build_info, options={}) {
},
singular_name: '{% trans "build output" %}',
plural_name: '{% trans "build outputs" %}',
custom_actions: [{
label: 'buildoutput',
icon: 'fa-tools',
title: '{% trans "Build output actions" %}',
actions: makeBuildOutputActions(build_info),
}]
});
// Request list of required tests for the part being assembled
@ -1383,25 +1453,6 @@ function loadBuildOutputTable(build_info, options={}) {
);
});
// Complete multiple outputs
$('#multi-output-complete').click(function() {
var outputs = getTableData(table);
completeBuildOutputs(
build_info.pk,
outputs,
{
success: function() {
// Reload the "in progress" table
$('#build-output-table').bootstrapTable('refresh');
// Reload the "completed" table
$('#build-stock-table').bootstrapTable('refresh');
}
}
);
});
// Delete multiple build outputs
$('#multi-output-delete').click(function() {
var outputs = getTableData(table);
@ -1421,25 +1472,6 @@ function loadBuildOutputTable(build_info, options={}) {
);
});
// Scrap multiple outputs
$('#multi-output-scrap').click(function() {
var outputs = getTableData(table);
scrapBuildOutputs(
build_info.pk,
outputs,
{
success: function() {
// Reload the "in progress" table
$('#build-output-table').bootstrapTable('refresh');
// Reload the "completed" table
$('#build-stock-table').bootstrapTable('refresh');
}
}
);
});
$('#outputs-expand').click(function() {
$(table).bootstrapTable('expandAllRows');
});
@ -2180,13 +2212,6 @@ function loadBuildTable(table, options) {
}
}
});
linkButtonsToSelection(
table,
[
'#build-print-options',
]
);
}

View File

@ -18,6 +18,7 @@
makeDeleteButton,
makeEditButton,
makeIconBadge,
orderParts,
renderClipboard,
renderDate,
renderLink,
@ -1213,6 +1214,43 @@ function deleteManufacturerPartParameters(selections, options={}) {
}
// Construct a set of actions for the manufacturer part table
function makeManufacturerPartActions(options={}) {
return [
{
label: 'order',
title: '{% trans "Order parts" %}',
icon: 'fa-shopping-cart',
permission: 'purchase_order.add',
callback: function(data) {
let parts = [];
data.forEach(function(item) {
let part = item.part_detail;
part.manufacturer_part = item.pk;
parts.push(part);
});
orderParts(parts);
},
},
{
label: 'delete',
title: '{% trans "Delete manufacturer parts" %}',
icon: 'fa-trash-alt icon-red',
permission: 'purchase_order.delete',
callback: function(data) {
deleteManufacturerParts(data, {
success: function() {
$('#manufacturer-part-table').bootstrapTable('refresh');
}
});
},
}
];
}
/*
* Load manufacturer part table
*/
@ -1226,7 +1264,18 @@ function loadManufacturerPartTable(table, url, options) {
var filterTarget = options.filterTarget || '#filter-list-manufacturer-part';
setupFilterList('manufacturer-part', $(table), filterTarget);
setupFilterList('manufacturer-part', $(table), filterTarget, {
custom_actions: [
{
label: 'manufacturer-part',
title: '{% trans "Manufacturer part actions" %}',
icon: 'fa-tools',
actions: makeManufacturerPartActions({
manufacturer_id: options.params.manufacturer,
})
}
]
});
$(table).inventreeTable({
url: url,
@ -1453,6 +1502,43 @@ function loadManufacturerPartParameterTable(table, url, options) {
}
// Construct a set of actions for the supplier part table
function makeSupplierPartActions(options={}) {
return [
{
label: 'order',
title: '{% trans "Order parts" %}',
icon: 'fa-shopping-cart',
permission: 'purchase_order.add',
callback: function(data) {
let parts = []
data.forEach(function(entry) {
parts.push(entry.part_detail);
});
orderParts(parts, {
supplier: options.supplier_id,
});
},
},
{
label: 'delete',
title: '{% trans "Delete supplier parts" %}',
icon: 'fa-trash-alt icon-red',
permission: 'purchase_order.delete',
callback: function(data) {
deleteSupplierParts(data, {
success: function() {
$('#supplier-part-table').bootstrapTable('refresh');
}
});
},
}
];
}
/*
* Load supplier part table
*/
@ -1464,7 +1550,18 @@ function loadSupplierPartTable(table, url, options) {
// Load filters
var filters = loadTableFilters('supplierpart', params);
setupFilterList('supplierpart', $(table));
setupFilterList('supplierpart', $(table), '#filter-list-supplier-part', {
custom_actions: [
{
label: 'supplier-part',
title: '{% trans "Supplier part actions" %}',
icon: 'fa-tools',
actions: makeSupplierPartActions({
supplier_id: options.params.supplier,
}),
}
]
});
$(table).inventreeTable({
url: url,

View File

@ -1,6 +1,7 @@
{% load i18n %}
/* globals
checkPermission,
downloadTableData,
getAvailableTableFilters,
getTableData,
@ -266,6 +267,102 @@ function generateFilterInput(tableKey, filterKey) {
}
/*
* Construct a single action button based on the provided definition
*/
function makeFilterActionButton(button, options={}) {
let prefix = options.prefix || 'action';
// Check for required permission (if specified)
if (button.permission) {
if (!checkPermission(button.permission)) {
return '';
}
}
return `
<li><a class='dropdown-item' href='#' id='action-${prefix}-${button.label}'>
<span class='fas ${button.icon}'></span> ${button.title}
</a></li>`;
}
/*
* Construct a set of custom actions for a given table
*/
function makeCustomActionGroup(action_group, table) {
let buttons = [];
let label = action_group.label || 'actions';
let title = action_group.title || '{% trans "Actions" %}';
let icon = action_group.icon || 'fa-tools';
// Construct the HTML for each button
action_group.actions.forEach(function(action) {
buttons.push(makeFilterActionButton(action, {prefix: label}));
});
if (buttons.length == 0) {
// Don't display anything if there are no buttons to show
return '';
}
let html = `
<div class='btn-group' role='group'>
<button id='${label}-actions' title='${title}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
<span class='fas ${icon}'></span>
</button>
<ul class='dropdown-menu' role='menu'>
`;
buttons.forEach(function(button) {
html += button;
});
html += `</ul></div>`;
return html;
}
/*
* Construct a set of custom barcode actions for a given table
*
* To define barcode actions for a data table, use options.barcode_actions
*/
function makeBarcodeActions(barcode_actions, table) {
let html = `
<div class='btn-group' role='group'>
<button id='barcode-actions' title='{% trans "Barcode actions" %}' class='btn btn-outline-secondary dropdown-toggle' type='button' data-bs-toggle='dropdown'>
<span class='fas fa-qrcode'></span>
</button>
<ul class='dropdown-menu' role='menu'>
`;
barcode_actions.forEach(function(action) {
html += makeFilterActionButton(action, {prefix: 'barcode'});
});
html += `</ul></div>`;
return html;
}
/*
* Add callbacks for custom actions
*/
function addFilterActionCallbacks(element, label, table, actions) {
actions.forEach(function(action) {
let id = `action-${label}-${action.label}`;
element.find(`#${id}`).click(function() {
let data = getTableData(table);
action.callback(data);
});
});
}
/*
* Helper function to make a 'filter' style button
*/
@ -315,6 +412,19 @@ function setupFilterList(tableKey, table, target, options={}) {
let report_button = options.report && global_settings.REPORT_ENABLE;
let labels_button = options.labels && global_settings.LABEL_ENABLE;
let barcode_actions = options.barcode_actions && global_settings.BARCODE_ENABLE;
// Add in "custom" actions first (to the left of the table buttons)
if (options.custom_actions) {
options.custom_actions.forEach(function(action_group) {
buttons += makeCustomActionGroup(action_group, table);
});
}
// Add in button for custom barcode actions
if (barcode_actions) {
buttons += makeBarcodeActions(options.barcode_actions, table);
}
if (report_button || labels_button) {
let print_buttons = `
@ -394,6 +504,19 @@ function setupFilterList(tableKey, table, target, options={}) {
element.append(filter_tag);
}
// Callback for custom actions
if (options.custom_actions) {
options.custom_actions.forEach(function(action_group) {
let label = action_group.label || 'actions';
addFilterActionCallbacks(element, label, table, action_group.actions);
});
}
// Callback for barcode actions
if (barcode_actions) {
addFilterActionCallbacks(element, 'barcode', table, options.barcode_actions);
}
// Callback for printing reports
if (options.report && global_settings.REPORT_ENABLE) {
element.find(`#print-report-${tableKey}`).click(function() {

View File

@ -23,7 +23,6 @@
inventreeLoad,
inventreePut,
inventreeSave,
linkButtonsToSelection,
loadTableFilters,
makeDeleteButton,
makeEditButton,
@ -2159,6 +2158,69 @@ function partGridTile(part) {
}
/*
* Update the category for a set of parts
*/
function setPartCategory(data, options={}) {
let parts = [];
data.forEach(function(item) {
parts.push(item.pk);
});
var html = `
<div class='alert alert-block alert-info'>
{% trans "Set the part category for the selected parts" %}
</div>
`;
constructForm('{% url "api-part-change-category" %}',{
title: '{% trans "Set Part Category" %}',
method: 'POST',
preFormContent: html,
fields: {
category: {},
},
processBeforeUpload: function(data) {
data.parts = parts;
return data;
},
onSuccess: function() {
$(options.table).bootstrapTable('refresh');
}
});
}
/*
* Construct a set of custom actions for the part table
*/
function makePartActions(table) {
return [
{
label: 'set-category',
title: '{% trans "Set category" %}',
icon: 'fa-sitemap',
permission: 'part.change',
callback: function(data) {
setPartCategory(data, {table: table});
}
},
{
label: 'order',
title: '{% trans "Order parts" %}',
icon: 'fa-shopping-cart',
permission: 'purchase_order.add',
callback: function(data) {
orderParts(data);
},
}
]
}
/* Load part listing data into specified table.
*
* Args:
@ -2190,6 +2252,14 @@ function loadPartTable(table, url, options={}) {
},
singular_name: '{% trans "part" %}',
plural_name: '{% trans "parts" %}',
custom_actions: [
{
label: 'parts',
icon: 'fa-tools',
title: '{% trans "Part actions" %}',
actions: makePartActions(table),
}
]
});
var columns = [
@ -2438,97 +2508,6 @@ function loadPartTable(table, url, options={}) {
return html;
}
});
if (options.buttons) {
linkButtonsToSelection($(table), options.buttons);
}
/* Button callbacks for part table buttons */
// Callback function for the "order parts" button
$('#multi-part-order').click(function() {
var selections = getTableData(table);
var parts = [];
selections.forEach(function(part) {
parts.push(part);
});
orderParts(parts, {});
});
// Callback function for the "set category" button
$('#multi-part-category').click(function() {
var selections = getTableData(table);
var parts = [];
selections.forEach(function(item) {
parts.push(item.pk);
});
var html = `
<div class='alert alert-block alert-info'>
{% trans "Set the part category for the selected parts" %}
</div>
`;
constructFormBody({}, {
title: '{% trans "Set Part Category" %}',
preFormContent: html,
fields: {
category: {
label: '{% trans "Category" %}',
help_text: '{% trans "Select Part Category" %}',
required: true,
type: 'related field',
model: 'partcategory',
api_url: '{% url "api-part-category-list" %}',
}
},
onSubmit: function(fields, opts) {
var category = getFormFieldValue('category', fields['category'], opts);
if (category == null) {
handleFormErrors(
{
'category': ['{% trans "Category is required" %}']
},
opts.fields,
opts
);
return;
}
// Set the category for each part in sequence
function setCategory() {
if (parts.length > 0) {
var part = parts.shift();
inventreePut(
`{% url "api-part-list" %}${part}/`,
{
category: category,
},
{
method: 'PATCH',
complete: setCategory,
}
);
} else {
// We are done!
$(opts.modal).modal('hide');
$(table).bootstrapTable('refresh');
}
}
// Start the ball rolling
showModalSpinner(opts.modal);
setCategory();
},
});
});
}

View File

@ -31,7 +31,6 @@
inventreeLoad,
inventreePut,
launchModalForm,
linkButtonsToSelection,
loadTableFilters,
makeCopyButton,
makeDeleteButton,
@ -606,7 +605,7 @@ function newSupplierPartFromOrderWizard(e) {
/*
* Create a new form to order parts based on the list of provided parts.
*/
function orderParts(parts_list, options) {
function orderParts(parts_list, options={}) {
var parts = [];
@ -786,7 +785,7 @@ function orderParts(parts_list, options) {
supplier_part_filters.manufacturer_part = options.manufacturer_part;
}
// Construct API filtres for the PurchaseOrder field
// Construct API filters for the PurchaseOrder field
var order_filters = {
status: {{ PurchaseOrderStatus.PENDING }},
supplier_detail: true,
@ -822,6 +821,10 @@ function orderParts(parts_list, options) {
$(opts.modal).find(`#info-pack-size-${pk}`).remove();
if (typeof value === 'object') {
value = value.pk;
}
if (value != null) {
inventreeGet(
`/api/company/part/${value}/`,
@ -1866,14 +1869,9 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
var filters = loadTableFilters('purchaseorderlineitem', options.params);
setupFilterList(
'purchaseorderlineitem',
$(table),
options.filter_target || '#filter-list-purchase-order-lines',
{
download: true
}
);
setupFilterList('purchaseorderlineitem', $(table), options.filter_target || '#filter-list-purchase-order-lines', {
download: true,
});
function setupCallbacks() {
if (options.allow_edit) {
@ -2211,12 +2209,4 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
}
]
});
linkButtonsToSelection(
table,
[
'#multi-select-options',
]
);
}

View File

@ -1,6 +1,7 @@
{% load i18n %}
/* globals
checkPermission,
getModelRenderer,
inventreeGet,
inventreePut,
@ -22,40 +23,6 @@ function closeSearchPanel() {
}
// Keep track of the roles / permissions available to the current user
var search_user_roles = null;
/*
* Check if the user has the specified role and permission
*/
function checkPermission(role, permission='view') {
if (!search_user_roles) {
return false;
}
if (!(role in search_user_roles)) {
return false;
}
var roles = search_user_roles[role];
if (!roles) {
return false;
}
var found = false;
search_user_roles[role].forEach(function(p) {
if (String(p).valueOf() == String(permission).valueOf()) {
found = true;
}
});
return found;
}
/*
* Callback when the search panel is opened.
@ -71,15 +38,6 @@ function openSearchPanel() {
clearSearchResults();
// Request user roles if we do not have them
if (search_user_roles == null) {
inventreeGet('{% url "api-user-roles" %}', {}, {
success: function(response) {
search_user_roles = response.roles || {};
}
});
}
// Callback for text input changed
search_input.on('keyup change', searchTextChanged);

View File

@ -30,7 +30,6 @@
inventreePut,
inventreeSave,
launchModalForm,
linkButtonsToSelection,
loadTableFilters,
makeDeleteButton,
makeEditButton,
@ -1707,6 +1706,121 @@ function locationDetail(row, showLink=true) {
}
/*
* Construct a set of custom actions for the stock table
*/
function makeStockActions(table) {
let actions = [
{
label: 'add',
icon: 'fa-plus-circle icon-green',
title: '{% trans "Add stock" %}',
permission: 'stock.change',
callback: function(data) {
stockAdjustment('add', data, table);
}
},
{
label: 'remove',
icon: 'fa-minus-circle icon-red',
title: '{% trans "Remove stock" %}',
permission: 'stock.change',
callback: function(data) {
stockAdjustment('take', data, table);
},
},
{
label: 'stocktake',
icon: 'fa-check-circle icon-blue',
title: '{% trans "Count stock" %}',
permission: 'stock.change',
callback: function(data) {
stockAdjustment('count', data, table);
},
},
{
label: 'move',
icon: 'fa-exchange-alt icon-blue',
title: '{% trans "Transfer stock" %}',
permission: 'stock.change',
callback: function(data) {
stockAdjustment('move', data, table);
}
},
{
label: 'status',
icon: 'fa-info-circle icon-blue',
title: '{% trans "Change stock status" %}',
permission: 'stock.change',
callback: function(data) {
setStockStatus(data, {table: table});
},
},
{
label: 'merge',
icon: 'fa-object-group',
title: '{% trans "Merge stock" %}',
permission: 'stock.change',
callback: function(data) {
mergeStockItems(data, {
success: function(response) {
$(table).bootstrapTable('refresh');
showMessage('{% trans "Merged stock items" %}', {
style: 'success',
});
}
});
},
},
{
label: 'order',
icon: 'fa-shopping-cart',
title: '{% trans "Order stock" %}',
permission: 'stock.change',
callback: function(data) {
let parts = [];
data.forEach(function(item) {
var part = item.part_detail;
if (part) {
parts.push(part);
}
});
orderParts(parts, {});
},
},
{
label: 'assign',
icon: 'fa-user-tie',
title: '{% trans "Assign to customer" %}',
permission: 'stock.change',
callback: function(data) {
assignStockToCustomer(data, {
success: function() {
$(table).bootstrapTable('refresh');
}
});
},
},
{
label: 'delete',
icon: 'fa-trash-alt icon-red',
title: '{% trans "Delete stock" %}',
permission: 'stock.delete',
callback: function(data) {
stockAdjustment('delete', data, table);
},
}
];
return actions;
}
/* Load data into a stock table with adjustable options.
* Fetches data (via AJAX) and loads into a bootstrap table.
* Also links in default button callbacks.
@ -1746,7 +1860,26 @@ function loadStockTable(table, options) {
key: 'item',
},
singular_name: '{% trans "stock item" %}',
plural_name: '{% trans "stock items" %}'
plural_name: '{% trans "stock items" %}',
barcode_actions: [
{
icon: 'fa-sitemap',
label: 'scantolocation',
title: '{% trans "Scan to location" %}',
permission: 'stock.change',
callback: function(items) {
scanItemsIntoLocation(items);
}
}
],
custom_actions: [
{
actions: makeStockActions(table),
icon: 'fa-boxes',
title: '{% trans "Stock Actions" %}',
label: 'stock',
}
]
});
// Override the default values, or add new ones
@ -2267,114 +2400,6 @@ function loadStockTable(table, options) {
buttons.push('#stock-barcode-options');
}
linkButtonsToSelection(
table,
buttons,
);
function stockAdjustment(action) {
var items = getTableData(table);
adjustStock(action, items, {
success: function() {
$(table).bootstrapTable('refresh');
}
});
}
// Automatically link button callbacks
if (global_settings.BARCODE_ENABLE) {
$('#multi-item-barcode-scan-into-location').click(function() {
var selections = getTableData(table);
var items = [];
selections.forEach(function(item) {
items.push(item);
});
scanItemsIntoLocation(items);
});
}
// Callback for 'stocktake' button
$('#multi-item-stocktake').click(function() {
stockAdjustment('count');
});
// Callback for 'remove stock' button
$('#multi-item-remove').click(function() {
stockAdjustment('take');
});
// Callback for 'add stock' button
$('#multi-item-add').click(function() {
stockAdjustment('add');
});
// Callback for 'move stock' button
$('#multi-item-move').click(function() {
stockAdjustment('move');
});
// Callback for 'merge stock' button
$('#multi-item-merge').click(function() {
var items = getTableData(table);
mergeStockItems(items, {
success: function(response) {
$(table).bootstrapTable('refresh');
showMessage('{% trans "Merged stock items" %}', {
style: 'success',
});
}
});
});
// Callback for 'assign stock' button
$('#multi-item-assign').click(function() {
var items = getTableData(table);
assignStockToCustomer(items, {
success: function() {
$(table).bootstrapTable('refresh');
}
});
});
// Callback for 'un-assign stock' button
$('#multi-item-order').click(function() {
var selections = getTableData(table);
var parts = [];
selections.forEach(function(item) {
var part = item.part_detail;
if (part) {
parts.push(part);
}
});
orderParts(parts, {});
});
// Callback for 'delete stock' button
$('#multi-item-delete').click(function() {
var selections = getTableData(table);
var stock = [];
selections.forEach(function(item) {
stock.push(item.pk);
});
stockAdjustment('delete');
});
// Callback for 'change status' button
$('#multi-item-status').click(function() {
let selections = getTableData(table);
@ -2384,35 +2409,9 @@ function loadStockTable(table, options) {
items.push(item.pk);
});
if (items.length == 0) {
showAlertDialog(
'{% trans "Select Stock Items" %}',
'{% trans "Select one or more stock items" %}'
);
return;
}
let html = `
<div class='alert alert-info alert-block>
{% trans "Selected stock items" %}: ${items.length}
</div>`;
constructForm('{% url "api-stock-change-status" %}', {
title: '{% trans "Change Stock Status" %}',
method: 'POST',
preFormContent: html,
fields: {
status: {},
note: {},
},
processBeforeUpload: function(data) {
data.items = items;
return data;
},
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
});
});
}
@ -2917,10 +2916,6 @@ function loadStockTrackingTable(table, options) {
url: options.url,
});
if (options.buttons) {
linkButtonsToSelection(table, options.buttons);
}
table.on('click', '.btn-entry-edit', function() {
var button = $(this);
@ -3131,3 +3126,55 @@ function installStockItem(stock_item_id, part_id, options={}) {
}
);
}
// Perform the specified stock adjustment action against the selected items
function stockAdjustment(action, items, table) {
adjustStock(action, items, {
success: function() {
$(table).bootstrapTable('refresh');
}
});
}
/*
* Set the status of the selected stock items
*/
function setStockStatus(items, options={}) {
if (items.length == 0) {
showAlertDialog(
'{% trans "Select Stock Items" %}',
'{% trans "Select one or more stock items" %}'
);
return;
}
let id_values = [];
items.forEach(function(item) {
id_values.push(item.pk)
});
let html = `
<div class='alert alert-info alert-block>
{% trans "Selected stock items" %}: ${items.length}
</div>`;
constructForm('{% url "api-stock-change-status" %}', {
title: '{% trans "Change Stock Status" %}',
method: 'POST',
preFormContent: html,
fields: {
status: {},
note: {},
},
processBeforeUpload: function(data) {
data.items = items;
return data;
},
onSuccess: function() {
$(options.table).bootstrapTable('refresh');
}
});
}

View File

@ -52,6 +52,12 @@ function constructHasProjectCodeFilter() {
}
// Reset a dictionary of filters for the attachment table
function getAttachmentFilters() {
return {};
}
// Return a dictionary of filters for the return order table
function getReturnOrderFilters() {
var filters = {
@ -487,6 +493,11 @@ function getBuildTableFilters() {
}
function getBuildItemTableFilters() {
return {};
}
// Return a dictionary of filters for the "build lines" table
function getBuildLineTableFilters() {
return {
@ -770,6 +781,16 @@ function getAvailableTableFilters(tableKey) {
tableKey = tableKey.toLowerCase();
switch (tableKey) {
case 'attachments':
return getAttachmentFilters();
case 'build':
return getBuildTableFilters();
case 'builditems':
return getBuildItemTableFilters();
case 'buildlines':
return getBuildLineTableFilters();
case 'bom':
return getBOMTableFilters();
case 'category':
return getPartCategoryFilters();
case 'company':
@ -778,12 +799,6 @@ function getAvailableTableFilters(tableKey) {
return getContactFilters();
case 'customerstock':
return getCustomerStockFilters();
case 'bom':
return getBOMTableFilters();
case 'build':
return getBuildTableFilters();
case 'buildlines':
return getBuildLineTableFilters();
case 'location':
return getStockLocationFilters();
case 'parameters':

View File

@ -38,10 +38,10 @@ function reloadBootstrapTable(table) {
if (tbl.exists()) {
tbl.bootstrapTable('refresh');
} else {
console.error(`Invalid table name passed to reloadTable(): ${table}`);
console.error(`Invalid table name passed to reloadBootstrapTable(): ${table}`);
}
} else {
console.error(`Null value passed to reloadTable()`);
console.error(`Null value passed to reloadBootstrapTable()`);
}
}
@ -220,25 +220,6 @@ function enableButtons(elements, enabled) {
}
/* Link a bootstrap-table object to one or more buttons.
* The buttons will only be enabled if there is at least one row selected
*/
function linkButtonsToSelection(table, buttons) {
if (typeof table === 'string') {
table = $(table);
}
// Initially set the enable state of the buttons
enableButtons(buttons, table.bootstrapTable('getSelections').length > 0);
// Add a callback
table.on('check.bs.table uncheck.bs.table check-some.bs.table uncheck-some.bs.table check-all.bs.table uncheck-all.bs.table', function() {
enableButtons(buttons, table.bootstrapTable('getSelections').length > 0);
});
}
/**
* Returns true if the input looks like a valid number
* @param {String} n
@ -474,11 +455,6 @@ $.fn.inventreeTable = function(options) {
console.error(`Could not get list of visible columns for table '${tableName}'`);
}
}
// Optionally, link buttons to the table selection
if (options.buttons) {
linkButtonsToSelection(table, options.buttons);
}
};

View File

@ -9,46 +9,7 @@
{% endif %}
<div id='{{ prefix }}button-toolbar'>
<div class='button-toolbar container-fluid' style='float: right;'>
<div class='btn-group' role='group'>
{% if barcodes %}
<!-- Barcode actions menu -->
<div class='btn-group' role='group'>
<button id='stock-barcode-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle='dropdown' title='{% trans "Barcode Actions" %}'>
<span class='fas fa-qrcode'></span> <span class='caret'></span>
</button>
<ul class='dropdown-menu'>
<li><a class='dropdown-item' href='#' id='multi-item-barcode-scan-into-location' title='{% trans "Scan to Location" %}'><span class='fas fa-sitemap'></span> {% trans "Scan to Location" %}</a></li>
</ul>
</div>
{% endif %}
{% if not read_only %}
{% if roles.stock.change or roles.stock.delete %}
<div class="btn-group" role="group">
<button id='stock-options' class="btn btn-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" title='{% trans "Stock Options" %}'>
<span class='fas fa-boxes'></span> <span class="caret"></span>
</button>
<ul class="dropdown-menu">
{% if roles.stock.change %}
<li><a class='dropdown-item' href="#" id='multi-item-add' title='{% trans "Add to selected stock items" %}'><span class='fas fa-plus-circle icon-green'></span> {% trans "Add stock" %}</a></li>
<li><a class='dropdown-item' href="#" id='multi-item-remove' title='{% trans "Remove from selected stock items" %}'><span class='fas fa-minus-circle icon-red'></span> {% trans "Remove stock" %}</a></li>
<li><a class='dropdown-item' href="#" id='multi-item-stocktake' title='{% trans "Stocktake selected stock items" %}'><span class='fas fa-check-circle icon-green'></span> {% trans "Count stock" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-item-move' title='{% trans "Move selected stock items" %}'><span class='fas fa-exchange-alt icon-blue'></span> {% trans "Transfer stock" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-item-status' title='{% trans "Change stock status" %}'><span class='fas fa-info-circle icon-blue'></span> {% trans "Change stock status" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-item-merge' title='{% trans "Merge selected stock items" %}'><span class='fas fa-object-group'></span> {% trans "Merge stock" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
{% endif %}
{% if roles.stock.delete %}
<li><a class='dropdown-item' href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete stock" %}</a></li>
{% endif %}
</ul>
</div>
{% endif %}
{% endif %}
{% include "filter_list.html" with prefix=prefix id="stock" %}
</div>
</div>
{% include "filter_list.html" with prefix=prefix id="stock" %}
</div>
<table class='table table-striped table-condensed' data-toolbar='#{{ prefix }}button-toolbar' id='{{ prefix }}stock-table'>