mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #375 from SchrodingersGat/group-stock-items
Group stock items
This commit is contained in:
commit
da4f68e5a5
@ -104,7 +104,6 @@ MIDDLEWARE = [
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'InvenTree.middleware.AuthRequiredMiddleware',
|
||||
'InvenTree.middleware.QueryCountMiddleware',
|
||||
]
|
||||
|
||||
if DEBUG:
|
||||
|
@ -46,6 +46,7 @@
|
||||
location_detail: true,
|
||||
part_detail: true,
|
||||
},
|
||||
groupByField: 'location',
|
||||
buttons: [
|
||||
'#stock-options',
|
||||
],
|
||||
|
11
InvenTree/static/css/bootstrap-table-group-by.css
vendored
Normal file
11
InvenTree/static/css/bootstrap-table-group-by.css
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.bootstrap-table .table > tbody > tr.groupBy {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bootstrap-table .table > tbody > tr.groupBy.expanded {
|
||||
|
||||
}
|
||||
|
||||
.bootstrap-table .table > tbody > tr.hidden + tr.detail-view {
|
||||
display: none;
|
||||
}
|
283
InvenTree/static/css/bootstrap-table.css
vendored
283
InvenTree/static/css/bootstrap-table.css
vendored
File diff suppressed because one or more lines are too long
@ -43,6 +43,12 @@
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
/* Bootstrap table overrides */
|
||||
|
||||
.stock-sub-group td {
|
||||
background-color: #ebf4f4;
|
||||
}
|
||||
|
||||
/* Force select2 elements in modal forms to be full width */
|
||||
.select-full-width {
|
||||
width: 100%;
|
||||
|
269
InvenTree/static/script/bootstrap/bootstrap-table-group-by.js
vendored
Normal file
269
InvenTree/static/script/bootstrap/bootstrap-table-group-by.js
vendored
Normal file
@ -0,0 +1,269 @@
|
||||
(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;
|
||||
|
||||
function isNumeric(n) {
|
||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||
}
|
||||
|
||||
BootstrapTable.prototype.initSort = function () {
|
||||
_initSort.apply(this, Array.prototype.slice.apply(arguments));
|
||||
|
||||
var that = this;
|
||||
tableGroups = [];
|
||||
|
||||
/* Sort the items into groups */
|
||||
|
||||
if (this.options.groupBy && this.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 = {};
|
||||
}
|
||||
|
||||
if (value.length > 1) {
|
||||
item._data['parent-index'] = index;
|
||||
} else {
|
||||
item._data['parent-index'] = null;
|
||||
}
|
||||
|
||||
item._data['group-data'] = value;
|
||||
item._data['table'] = that;
|
||||
});
|
||||
|
||||
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('<tr class="info groupBy expanded" data-group-index="%s">', item.id));
|
||||
|
||||
if (that.options.detailView && !that.options.cardView) {
|
||||
html.push('<td class="detail"></td>');
|
||||
}
|
||||
|
||||
if (checkBox) {
|
||||
html.push('<td class="bs-checkbox">', '<input name="btSelectGroup" type="checkbox" />', '</td>');
|
||||
}
|
||||
|
||||
cols.forEach(function(col) {
|
||||
var cell = '<td>';
|
||||
|
||||
if (typeof that.options.groupByFormatter == 'function') {
|
||||
cell += '<i>' + that.options.groupByFormatter(col.field, item.id, item.data) + "</i>";
|
||||
}
|
||||
|
||||
cell += "</td>";
|
||||
|
||||
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('<td', sprintf(' colspan="%s"', visibleColumns), '>', formattedValue, '</td>');
|
||||
|
||||
cols.forEach(function(col) {
|
||||
html.push('<td>' + item.data[0][col.field] + '</td>');
|
||||
});
|
||||
*/
|
||||
|
||||
html.push('</tr>');
|
||||
|
||||
if(item.data.length > 1) {
|
||||
|
||||
that.$body.find('tr[data-parent-index=' + item.id + ']').addClass('hidden stock-sub-group');
|
||||
|
||||
// Insert the group header row before the first item
|
||||
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);
|
||||
});
|
3770
InvenTree/static/script/bootstrap/bootstrap-table.js
vendored
Normal file
3770
InvenTree/static/script/bootstrap/bootstrap-table.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -282,7 +282,7 @@ function moveStockItems(items, options) {
|
||||
for (i = 0; i < response.length; i++) {
|
||||
var loc = response[i];
|
||||
|
||||
html += makeOption(loc.pk, loc.name + ' - <i>' + loc.description + '</i>');
|
||||
html += makeOption(loc.pk, loc.pathstring + ' - <i>' + loc.description + '</i>');
|
||||
}
|
||||
|
||||
html += "</select><br>";
|
||||
@ -312,10 +312,18 @@ function moveStockItems(items, options) {
|
||||
|
||||
var item = items[i];
|
||||
|
||||
var name = item.part__IPN;
|
||||
|
||||
if (name) {
|
||||
name += ' | ';
|
||||
}
|
||||
|
||||
name += item.part__name;
|
||||
|
||||
html += "<tr>";
|
||||
|
||||
html += "<td>" + item.part.full_name + "</td>";
|
||||
html += "<td>" + item.location.pathstring + "</td>";
|
||||
html += "<td>" + name + "</td>";
|
||||
html += "<td>" + item.location__path + "</td>";
|
||||
html += "<td>" + item.quantity + "</td>";
|
||||
|
||||
html += "<td>";
|
||||
@ -372,14 +380,74 @@ 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,
|
||||
formatNoMatches: function() {
|
||||
return 'No stock items matching query';
|
||||
},
|
||||
customSort: customGroupSorter,
|
||||
groupBy: true,
|
||||
groupByField: options.groupByField || 'part',
|
||||
groupByFormatter: function(field, id, data) {
|
||||
|
||||
var row = data[0];
|
||||
|
||||
if (field == 'part__name') {
|
||||
|
||||
var name = row.part__IPN;
|
||||
|
||||
if (name) {
|
||||
name += ' | ';
|
||||
}
|
||||
|
||||
name += row.part__name;
|
||||
|
||||
return imageHoverIcon(row.part__image) + name + ' <i>(' + data.length + ' items)</i>';
|
||||
}
|
||||
else if (field == 'part__description') {
|
||||
return row.part__description;
|
||||
}
|
||||
else if (field == 'quantity') {
|
||||
var stock = 0;
|
||||
|
||||
data.forEach(function(item) {
|
||||
stock += item.quantity;
|
||||
});
|
||||
|
||||
return stock;
|
||||
} else if (field == 'location__path') {
|
||||
/* Determine how many locations */
|
||||
var locations = [];
|
||||
|
||||
data.forEach(function(item) {
|
||||
var loc = item.location;
|
||||
|
||||
if (!locations.includes(loc)) {
|
||||
locations.push(loc);
|
||||
}
|
||||
});
|
||||
|
||||
if (locations.length > 1) {
|
||||
return "In " + locations.length + " locations";
|
||||
} else {
|
||||
// A single location!
|
||||
return renderLink(row.location__path, '/stock/location/' + row.location + '/')
|
||||
}
|
||||
}
|
||||
else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
checkbox: true,
|
||||
@ -392,47 +460,66 @@ function loadStockTable(table, options) {
|
||||
visible: false,
|
||||
},
|
||||
{
|
||||
field: 'part_detail',
|
||||
field: 'part__name',
|
||||
title: 'Part',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index, field) {
|
||||
return imageHoverIcon(value.image_url) + renderLink(value.full_name, value.url + 'stock/');
|
||||
|
||||
var name = row.part__IPN;
|
||||
|
||||
if (name) {
|
||||
name += ' | ';
|
||||
}
|
||||
|
||||
name += row.part__name;
|
||||
|
||||
return imageHoverIcon(row.part__image) + renderLink(name, '/part/' + row.part + '/stock/');
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'part_detail.description',
|
||||
field: 'part__description',
|
||||
title: 'Description',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
field: 'location_detail',
|
||||
field: 'quantity',
|
||||
title: 'Stock',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index, field) {
|
||||
|
||||
var val = value;
|
||||
|
||||
// If there is a single unit with a serial number, use the serial number
|
||||
if (row.serial && row.quantity == 1) {
|
||||
val = '# ' + row.serial;
|
||||
}
|
||||
|
||||
var text = renderLink(val, '/stock/item/' + row.pk + '/');
|
||||
|
||||
text = text + "<span class='badge'>" + row.status_text + "</span>";
|
||||
return text;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'location__path',
|
||||
title: 'Location',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index, field) {
|
||||
if (value) {
|
||||
return renderLink(value.pathstring, value.url);
|
||||
return renderLink(value, '/stock/location/' + row.location + '/');
|
||||
}
|
||||
else {
|
||||
return '<i>No stock location set</i>';
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'quantity',
|
||||
title: 'Quantity',
|
||||
sortable: true,
|
||||
formatter: function(value, row, index, field) {
|
||||
var text = renderLink(value, row.url);
|
||||
text = text + "<span class='badge'>" + row.status_text + "</span>";
|
||||
return text;
|
||||
}
|
||||
},
|
||||
{
|
||||
field: 'notes',
|
||||
title: 'Notes',
|
||||
}
|
||||
],
|
||||
url: options.url,
|
||||
queryParams: params,
|
||||
});
|
||||
|
||||
if (options.buttons) {
|
||||
|
@ -37,3 +37,74 @@ function linkButtonsToSelection(table, buttons) {
|
||||
enableButtons(buttons, table.bootstrapTable('getSelections').length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function isNumeric(n) {
|
||||
return !isNaN(parseFloat(n)) && isFinite(n);
|
||||
}
|
||||
|
||||
|
||||
function customGroupSorter(sortName, sortOrder, sortData) {
|
||||
|
||||
console.log('got here');
|
||||
|
||||
var order = sortOrder === 'desc' ? -1 : 1;
|
||||
|
||||
sortData.sort(function(a, b) {
|
||||
|
||||
// Extract default field values
|
||||
var aa = a[sortName];
|
||||
var bb = b[sortName];
|
||||
|
||||
// Extract parent information
|
||||
var aparent = a._data && a._data['parent-index'];
|
||||
var bparent = b._data && b._data['parent-index'];
|
||||
|
||||
// If either of the comparisons are in a group
|
||||
if (aparent || bparent) {
|
||||
|
||||
// If the parents are different (or one item does not have a parent,
|
||||
// then we need to extract the parent value for the selected column.
|
||||
|
||||
if (aparent != bparent) {
|
||||
if (aparent) {
|
||||
aa = a._data['table'].options.groupByFormatter(sortName, 0, a._data['group-data']);
|
||||
}
|
||||
|
||||
if (bparent) {
|
||||
bb = b._data['table'].options.groupByFormatter(sortName, 0, b._data['group-data']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (aa === undefined || aa === null) {
|
||||
aa = '';
|
||||
}
|
||||
if (bb === undefined || bb === null) {
|
||||
bb = '';
|
||||
}
|
||||
|
||||
if (isNumeric(aa) && isNumeric(bb)) {
|
||||
if (aa < bb) {
|
||||
return order * -1;
|
||||
} else if (aa > bb) {
|
||||
return order;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
aa = aa.toString();
|
||||
bb = bb.toString();
|
||||
|
||||
var cmp = aa.localeCompare(bb);
|
||||
|
||||
if (cmp === -1) {
|
||||
return order * -1;
|
||||
} else if (cmp === 1) {
|
||||
return order;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
@ -5,6 +5,7 @@ JSON API for the Stock app
|
||||
from django_filters.rest_framework import FilterSet, DjangoFilterBackend
|
||||
from django_filters import NumberFilter
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import reverse
|
||||
|
||||
@ -20,6 +21,8 @@ from .serializers import StockTrackingSerializer
|
||||
from InvenTree.views import TreeSerializer
|
||||
from InvenTree.helpers import str2bool
|
||||
|
||||
import os
|
||||
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
@ -241,6 +244,7 @@ class StockList(generics.ListCreateAPIView):
|
||||
- POST: Create a new StockItem
|
||||
|
||||
Additional query parameters are available:
|
||||
- aggregate: If 'true' then stock items are aggregated by Part and Location
|
||||
- location: Filter stock by location
|
||||
- category: Filter by parts belonging to a certain category
|
||||
- supplier: Filter by supplier
|
||||
@ -257,6 +261,53 @@ class StockList(generics.ListCreateAPIView):
|
||||
kwargs['context'] = self.get_serializer_context()
|
||||
return self.serializer_class(*args, **kwargs)
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
# Instead of using the DRF serializer to LIST,
|
||||
# we will serialize the objects manually.
|
||||
# This is significantly faster
|
||||
|
||||
data = queryset.values(
|
||||
'pk',
|
||||
'quantity',
|
||||
'serial',
|
||||
'batch',
|
||||
'status',
|
||||
'notes',
|
||||
'location',
|
||||
'location__name',
|
||||
'location__description',
|
||||
'part',
|
||||
'part__IPN',
|
||||
'part__name',
|
||||
'part__description',
|
||||
'part__image',
|
||||
'part__category',
|
||||
'part__category__name',
|
||||
'part__category__description',
|
||||
)
|
||||
|
||||
# Reduce the number of lookups we need to do for categories
|
||||
# Cache location lookups for this query
|
||||
locations = {}
|
||||
|
||||
for item in data:
|
||||
item['part__image'] = os.path.join(settings.MEDIA_URL, item['part__image'])
|
||||
|
||||
loc_id = item['location']
|
||||
|
||||
if loc_id:
|
||||
if loc_id not in locations:
|
||||
locations[loc_id] = StockLocation.objects.get(pk=loc_id).pathstring
|
||||
|
||||
item['location__path'] = locations[loc_id]
|
||||
else:
|
||||
item['location__path'] = None
|
||||
|
||||
return Response(data)
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
If the query includes a particular location,
|
||||
@ -264,7 +315,7 @@ class StockList(generics.ListCreateAPIView):
|
||||
"""
|
||||
|
||||
# Start with all objects
|
||||
stock_list = StockItem.objects.all()
|
||||
stock_list = StockItem.objects.filter(customer=None, belongs_to=None)
|
||||
|
||||
# Does the client wish to filter by the Part ID?
|
||||
part_id = self.request.query_params.get('part', None)
|
||||
@ -310,8 +361,14 @@ class StockList(generics.ListCreateAPIView):
|
||||
if supplier_id:
|
||||
stock_list = stock_list.filter(supplier_part__supplier=supplier_id)
|
||||
|
||||
# Pre-fetch related objects for better response time
|
||||
stock_list = self.get_serializer_class().setup_eager_loading(stock_list)
|
||||
# Also ensure that we pre-fecth all the related items
|
||||
stock_list = stock_list.prefetch_related(
|
||||
'part',
|
||||
'part__category',
|
||||
'location'
|
||||
)
|
||||
|
||||
stock_list = stock_list.order_by('part__name')
|
||||
|
||||
return stock_list
|
||||
|
||||
|
@ -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'),
|
||||
|
@ -57,17 +57,10 @@ 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)
|
||||
|
||||
@staticmethod
|
||||
def setup_eager_loading(queryset):
|
||||
queryset = queryset.prefetch_related('part')
|
||||
queryset = queryset.prefetch_related('part__stock_items')
|
||||
queryset = queryset.prefetch_related('part__category')
|
||||
queryset = queryset.prefetch_related('location')
|
||||
|
||||
return queryset
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
||||
@ -88,6 +81,7 @@ class StockItemSerializer(serializers.ModelSerializer):
|
||||
'pk',
|
||||
'url',
|
||||
'part',
|
||||
'part_name',
|
||||
'part_detail',
|
||||
'supplier_part',
|
||||
'location',
|
||||
|
@ -30,6 +30,7 @@
|
||||
<!-- CSS -->
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/bootstrap-table-group-by.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/select2-bootstrap.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
|
||||
@ -87,8 +88,9 @@ InvenTree
|
||||
|
||||
<script type="text/javascript" src="{% static 'script/bootstrap/bootstrap.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-treeview.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-en-US.min.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/bootstrap/bootstrap-table-group-by.js' %}"></script>
|
||||
|
||||
<script type="text/javascript" src="{% static 'script/select2/select2.js' %}"></script>
|
||||
<script type='text/javascript' src="{% static 'script/moment.js' %}"></script>
|
||||
|
Loading…
Reference in New Issue
Block a user