From 4218cf8b45a3937da450b3b72d8d2d6411533cba Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 28 May 2019 17:21:29 +1000 Subject: [PATCH] Client-side grouping - Cusomizations made to bootstrap-table-group-by.js - Group by part name - Display total stock count per group - Only group if there are more than 1 item in the group - Groups send checkbox signals through appropriately! --- .../bootstrap/bootstrap-table-group-by.js | 263 ++++++++++++++++++ InvenTree/static/script/inventree/stock.js | 38 ++- InvenTree/stock/models.py | 3 + InvenTree/stock/serializers.py | 3 + InvenTree/templates/base.html | 1 + 5 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 InvenTree/static/script/bootstrap/bootstrap-table-group-by.js diff --git a/InvenTree/static/script/bootstrap/bootstrap-table-group-by.js b/InvenTree/static/script/bootstrap/bootstrap-table-group-by.js new file mode 100644 index 0000000000..4195ba394f --- /dev/null +++ b/InvenTree/static/script/bootstrap/bootstrap-table-group-by.js @@ -0,0 +1,263 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define([], factory); + } else if (typeof exports !== "undefined") { + factory(); + } else { + var mod = { + exports: {} + }; + factory(); + global.bootstrapTableGroupBy = mod.exports; + } +})(this, function () { + 'use strict'; + + /** + * @author: Yura Knoxville + * @version: v1.1.0 + */ + + (function ($) { + + 'use strict'; + + var initBodyCaller, tableGroups; + + // it only does '%s', and return '' when arguments are undefined + var sprintf = function sprintf(str) { + var args = arguments, + flag = true, + i = 1; + + str = str.replace(/%s/g, function () { + var arg = args[i++]; + + if (typeof arg === 'undefined') { + flag = false; + return ''; + } + return arg; + }); + return flag ? str : ''; + }; + + var groupBy = function groupBy(array, f) { + var groups = {}; + array.forEach(function (o) { + var group = f(o); + groups[group] = groups[group] || []; + groups[group].push(o); + }); + + return groups; + }; + + $.extend($.fn.bootstrapTable.defaults, { + groupBy: false, + groupByField: '', + groupByFormatter: undefined + }); + + var BootstrapTable = $.fn.bootstrapTable.Constructor, + _initSort = BootstrapTable.prototype.initSort, + _initBody = BootstrapTable.prototype.initBody, + _updateSelected = BootstrapTable.prototype.updateSelected; + + BootstrapTable.prototype.initSort = function () { + _initSort.apply(this, Array.prototype.slice.apply(arguments)); + + var that = this; + tableGroups = []; + + console.log('Sorting...'); + + console.log(typeof this.options.groupByField); + + if (this.options.groupBy && this.options.groupByField !== '') { + + if (1 || (this.options.sortName != this.options.groupByField)) { + this.data.sort(function (a, b) { + console.log('x'); + return a[that.options.groupByField].localeCompare(b[that.options.groupByField]); + }); + } + + var that = this; + var groups = groupBy(that.data, function (item) { + return [item[that.options.groupByField]]; + }); + + var index = 0; + $.each(groups, function (key, value) { + tableGroups.push({ + id: index, + name: key, + data: value + }); + + value.forEach(function (item) { + if (!item._data) { + item._data = {}; + } + + item._data['parent-index'] = index; + }); + + index++; + }); + } + }; + + BootstrapTable.prototype.initBody = function () { + initBodyCaller = true; + + _initBody.apply(this, Array.prototype.slice.apply(arguments)); + + if (this.options.groupBy && this.options.groupByField !== '') { + var that = this, + checkBox = false, + visibleColumns = 0; + + var cols = []; + + this.columns.forEach(function (column) { + if (column.checkbox) { + checkBox = true; + } else { + if (column.visible) { + visibleColumns += 1; + cols.push(column); + } + } + }); + + if (this.options.detailView && !this.options.cardView) { + visibleColumns += 1; + } + + tableGroups.forEach(function (item) { + var html = []; + + html.push(sprintf('', item.id)); + + if (that.options.detailView && !that.options.cardView) { + html.push(''); + } + + if (checkBox) { + html.push('', '', ''); + } + + cols.forEach(function(col) { + var cell = ''; + + if (typeof that.options.groupByFormatter == 'function') { + cell += that.options.groupByFormatter(col.title, item.id, item.data); + } + + cell += ""; + + html.push(cell); + }); + + /* + var formattedValue = item.name; + if (typeof that.options.groupByFormatter == "function") { + formattedValue = that.options.groupByFormatter(item.name, item.id, item.data); + } + html.push('', formattedValue, ''); + + cols.forEach(function(col) { + html.push('' + item.data[0][col.field] + ''); + }); + */ + + html.push(''); + + if(item.data.length > 1) { + that.$body.find('tr[data-parent-index=' + item.id + ']:first').before($(html.join(''))); + } + }); + + this.$selectGroup = []; + this.$body.find('[name="btSelectGroup"]').each(function () { + var self = $(this); + + that.$selectGroup.push({ + group: self, + item: that.$selectItem.filter(function () { + return $(this).closest('tr').data('parent-index') === self.closest('tr').data('group-index'); + }) + }); + }); + + this.$container.off('click', '.groupBy').on('click', '.groupBy', function () { + $(this).toggleClass('expanded'); + that.$body.find('tr[data-parent-index=' + $(this).closest('tr').data('group-index') + ']').toggleClass('hidden'); + }); + + this.$container.off('click', '[name="btSelectGroup"]').on('click', '[name="btSelectGroup"]', function (event) { + event.stopImmediatePropagation(); + + var self = $(this); + var checked = self.prop('checked'); + that[checked ? 'checkGroup' : 'uncheckGroup']($(this).closest('tr').data('group-index')); + }); + } + + initBodyCaller = false; + this.updateSelected(); + }; + + BootstrapTable.prototype.updateSelected = function () { + if (!initBodyCaller) { + _updateSelected.apply(this, Array.prototype.slice.apply(arguments)); + + if (this.options.groupBy && this.options.groupByField !== '') { + this.$selectGroup.forEach(function (item) { + var checkGroup = item.item.filter(':enabled').length === item.item.filter(':enabled').filter(':checked').length; + + item.group.prop('checked', checkGroup); + }); + } + } + }; + + BootstrapTable.prototype.getGroupSelections = function (index) { + var that = this; + + return $.grep(this.data, function (row) { + return row[that.header.stateField] && row._data['parent-index'] === index; + }); + }; + + BootstrapTable.prototype.checkGroup = function (index) { + this.checkGroup_(index, true); + }; + + BootstrapTable.prototype.uncheckGroup = function (index) { + this.checkGroup_(index, false); + }; + + BootstrapTable.prototype.checkGroup_ = function (index, checked) { + var rows; + var filter = function filter() { + return $(this).closest('tr').data('parent-index') === index; + }; + + if (!checked) { + rows = this.getGroupSelections(index); + } + + this.$selectItem.filter(filter).prop('checked', checked); + + this.updateRows(); + this.updateSelected(); + if (checked) { + rows = this.getGroupSelections(index); + } + this.trigger(checked ? 'check-all' : 'uncheck-all', rows); + }; + })(jQuery); +}); \ No newline at end of file diff --git a/InvenTree/static/script/inventree/stock.js b/InvenTree/static/script/inventree/stock.js index 0d18c064e9..66c1fcc1de 100644 --- a/InvenTree/static/script/inventree/stock.js +++ b/InvenTree/static/script/inventree/stock.js @@ -372,14 +372,45 @@ function moveStockItems(items, options) { function loadStockTable(table, options) { + var params = options.params || {}; + + // Aggregate stock items + //params.aggregate = true; + table.bootstrapTable({ sortable: true, search: true, method: 'get', pagination: true, - pageSize: 50, + pageSize: 25, rememberOrder: true, - queryParams: options.params, + groupBy: true, + groupByField: 'part_name', + groupByFields: ['part_name', 'test'], + groupByFormatter: function(field, id, data) { + + if (field == 'Part') { + return imageHoverIcon(data[0].part_detail.image_url) + + data[0].part_detail.full_name + + ' (' + data.length + ' items)'; + } + else if (field == 'Description') { + return data[0].part_detail.description; + } + else if (field == 'Stock') { + var stock = 0; + + data.forEach(function(item) { + stock += item.quantity; + }); + + return stock; + } + + else { + return ''; + } + }, columns: [ { checkbox: true, @@ -419,7 +450,7 @@ function loadStockTable(table, options) { }, { field: 'quantity', - title: 'Quantity', + title: 'Stock', sortable: true, formatter: function(value, row, index, field) { var text = renderLink(value, row.url); @@ -433,6 +464,7 @@ function loadStockTable(table, options) { } ], url: options.url, + queryParams: params, }); if (options.buttons) { diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index f6651f6dd4..4efa347ecd 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -183,6 +183,9 @@ class StockItem(models.Model): def get_absolute_url(self): return reverse('stock-item-detail', kwargs={'pk': self.id}) + def get_part_name(self): + return self.part.full_name + class Meta: unique_together = [ ('part', 'serial'), diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 6037d82031..21f2ef4e9e 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -57,6 +57,8 @@ class StockItemSerializer(serializers.ModelSerializer): url = serializers.CharField(source='get_absolute_url', read_only=True) status_text = serializers.CharField(source='get_status_display', read_only=True) + part_name = serializers.CharField(source='get_part_name', read_only=True) + part_detail = PartBriefSerializer(source='part', many=False, read_only=True) location_detail = LocationBriefSerializer(source='location', many=False, read_only=True) @@ -79,6 +81,7 @@ class StockItemSerializer(serializers.ModelSerializer): 'pk', 'url', 'part', + 'part_name', 'part_detail', 'supplier_part', 'location', diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index dd2f8bd706..69e72248d2 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -89,6 +89,7 @@ InvenTree +