mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-29 20:16:44 +00:00
Stock installed items (#5030)
* Cleanup * Add "installed_items" count to StockItem serializer - Add queryset annotation - Add API filter for list endpoint * js code * More js updates * Load installed items on demand * Make option configurable
This commit is contained in:
parent
013d206b91
commit
62faaf01c5
@ -1467,6 +1467,13 @@ class InvenTreeSetting(BaseInvenTreeSetting):
|
|||||||
'default': '',
|
'default': '',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'STOCK_SHOW_INSTALLED_ITEMS': {
|
||||||
|
'name': _('Show Installed Stock Items'),
|
||||||
|
'description': _('Display installed stock items in stock tables'),
|
||||||
|
'default': False,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
'BUILDORDER_REFERENCE_PATTERN': {
|
'BUILDORDER_REFERENCE_PATTERN': {
|
||||||
'name': _('Build Order Reference Pattern'),
|
'name': _('Build Order Reference Pattern'),
|
||||||
'description': _('Required pattern for generating Build Order reference field'),
|
'description': _('Required pattern for generating Build Order reference field'),
|
||||||
|
@ -510,6 +510,15 @@ class StockFilter(rest_filters.FilterSet):
|
|||||||
else:
|
else:
|
||||||
return queryset.filter(belongs_to=None)
|
return queryset.filter(belongs_to=None)
|
||||||
|
|
||||||
|
has_installed_items = rest_filters.BooleanFilter(label='Has installed items', method='filter_has_installed')
|
||||||
|
|
||||||
|
def filter_has_installed(self, queryset, name, value):
|
||||||
|
"""Filter stock items by "belongs_to" field being empty."""
|
||||||
|
if str2bool(value):
|
||||||
|
return queryset.filter(installed_items__gt=0)
|
||||||
|
else:
|
||||||
|
return queryset.filter(installed_items=0)
|
||||||
|
|
||||||
sent_to_customer = rest_filters.BooleanFilter(label='Sent to customer', method='filter_sent_to_customer')
|
sent_to_customer = rest_filters.BooleanFilter(label='Sent to customer', method='filter_sent_to_customer')
|
||||||
|
|
||||||
def filter_sent_to_customer(self, queryset, name, value):
|
def filter_sent_to_customer(self, queryset, name, value):
|
||||||
|
@ -133,14 +133,12 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
|
|
||||||
model = StockItem
|
model = StockItem
|
||||||
fields = [
|
fields = [
|
||||||
'allocated',
|
|
||||||
'batch',
|
'batch',
|
||||||
'belongs_to',
|
'belongs_to',
|
||||||
'build',
|
'build',
|
||||||
'consumed_by',
|
'consumed_by',
|
||||||
'customer',
|
'customer',
|
||||||
'delete_on_deplete',
|
'delete_on_deplete',
|
||||||
'expired',
|
|
||||||
'expiry_date',
|
'expiry_date',
|
||||||
'is_building',
|
'is_building',
|
||||||
'link',
|
'link',
|
||||||
@ -158,13 +156,11 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
'sales_order',
|
'sales_order',
|
||||||
'sales_order_reference',
|
'sales_order_reference',
|
||||||
'serial',
|
'serial',
|
||||||
'stale',
|
|
||||||
'status',
|
'status',
|
||||||
'status_text',
|
'status_text',
|
||||||
'stocktake_date',
|
'stocktake_date',
|
||||||
'supplier_part',
|
'supplier_part',
|
||||||
'supplier_part_detail',
|
'supplier_part_detail',
|
||||||
'tracking_items',
|
|
||||||
'barcode_hash',
|
'barcode_hash',
|
||||||
'updated',
|
'updated',
|
||||||
'purchase_price',
|
'purchase_price',
|
||||||
@ -172,6 +168,13 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
'use_pack_size',
|
'use_pack_size',
|
||||||
'tests',
|
'tests',
|
||||||
|
|
||||||
|
# Annotated fields
|
||||||
|
'allocated',
|
||||||
|
'expired',
|
||||||
|
'installed_items',
|
||||||
|
'stale',
|
||||||
|
'tracking_items',
|
||||||
|
|
||||||
'tags',
|
'tags',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -282,6 +285,11 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Annotate with the total number of "installed items"
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
installed_items=SubqueryCount('installed_parts')
|
||||||
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
status_text = serializers.CharField(source='get_status_display', read_only=True)
|
||||||
@ -295,10 +303,11 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
|
|||||||
quantity = InvenTreeDecimalField()
|
quantity = InvenTreeDecimalField()
|
||||||
|
|
||||||
# Annotated fields
|
# Annotated fields
|
||||||
tracking_items = serializers.IntegerField(read_only=True, required=False)
|
|
||||||
allocated = serializers.FloatField(required=False)
|
allocated = serializers.FloatField(required=False)
|
||||||
expired = serializers.BooleanField(required=False, read_only=True)
|
expired = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
installed_items = serializers.IntegerField(read_only=True, required=False)
|
||||||
stale = serializers.BooleanField(required=False, read_only=True)
|
stale = serializers.BooleanField(required=False, read_only=True)
|
||||||
|
tracking_items = serializers.IntegerField(read_only=True, required=False)
|
||||||
|
|
||||||
purchase_price = InvenTree.serializers.InvenTreeMoneySerializer(
|
purchase_price = InvenTree.serializers.InvenTreeMoneySerializer(
|
||||||
label=_('Purchase Price'),
|
label=_('Purchase Price'),
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
{% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" icon="fa-tools" %}
|
{% include "InvenTree/settings/setting.html" with key="STOCK_ALLOW_EXPIRED_BUILD" icon="fa-tools" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="STOCK_OWNERSHIP_CONTROL" icon="fa-users" %}
|
{% include "InvenTree/settings/setting.html" with key="STOCK_OWNERSHIP_CONTROL" icon="fa-users" %}
|
||||||
{% include "InvenTree/settings/setting.html" with key="STOCK_LOCATION_DEFAULT_ICON" icon="fa-icons" %}
|
{% include "InvenTree/settings/setting.html" with key="STOCK_LOCATION_DEFAULT_ICON" icon="fa-icons" %}
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="STOCK_SHOW_INSTALLED_ITEMS" icon="fa-sitemap" %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -1724,6 +1724,9 @@ function loadStockTable(table, options) {
|
|||||||
options.params['location_detail'] = true;
|
options.params['location_detail'] = true;
|
||||||
options.params['part_detail'] = true;
|
options.params['part_detail'] = true;
|
||||||
|
|
||||||
|
// Determine if installed items are displayed in the table
|
||||||
|
let show_installed_items = global_settings.STOCK_SHOW_INSTALLED_ITEMS;
|
||||||
|
|
||||||
var params = options.params || {};
|
var params = options.params || {};
|
||||||
|
|
||||||
const filterTarget = options.filterTarget || '#filter-list-stock';
|
const filterTarget = options.filterTarget || '#filter-list-stock';
|
||||||
@ -1769,18 +1772,35 @@ function loadStockTable(table, options) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
col = {
|
col = {
|
||||||
field: 'part_detail.full_name',
|
field: 'part',
|
||||||
title: '{% trans "Part" %}',
|
title: '{% trans "Part" %}',
|
||||||
sortName: 'part__name',
|
sortName: 'part__name',
|
||||||
visible: params['part_detail'],
|
visible: params['part_detail'],
|
||||||
switchable: params['part_detail'],
|
switchable: params['part_detail'],
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
|
|
||||||
return partDetail(row.part_detail, {
|
let html = '';
|
||||||
|
|
||||||
|
if (show_installed_items && row.installed_items > 0) {
|
||||||
|
if (row.installed_items_received) {
|
||||||
|
// Data received, ignore
|
||||||
|
} else if (row.installed_items_requested) {
|
||||||
|
html += `<span class='fas fa-sync fa-spin'></span>`;
|
||||||
|
} else {
|
||||||
|
html += `
|
||||||
|
<a href='#' pk='${row.pk}' class='load-sub-items' id='load-sub-items-${row.pk}'>
|
||||||
|
<span class='fas fa-sync-alt' title='{% trans "Load installed items" %}'></span>
|
||||||
|
</a>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html += partDetail(row.part_detail, {
|
||||||
thumb: true,
|
thumb: true,
|
||||||
link: true,
|
link: true,
|
||||||
icons: true,
|
icons: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1791,7 +1811,7 @@ function loadStockTable(table, options) {
|
|||||||
columns.push(col);
|
columns.push(col);
|
||||||
|
|
||||||
col = {
|
col = {
|
||||||
field: 'part_detail.IPN',
|
field: 'IPN',
|
||||||
title: '{% trans "IPN" %}',
|
title: '{% trans "IPN" %}',
|
||||||
sortName: 'part__IPN',
|
sortName: 'part__IPN',
|
||||||
visible: params['part_detail'],
|
visible: params['part_detail'],
|
||||||
@ -2164,6 +2184,37 @@ function loadStockTable(table, options) {
|
|||||||
title: '{% trans "Notes" %}',
|
title: '{% trans "Notes" %}',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Function to request subset of items which are installed *within* a particular item
|
||||||
|
function requestInstalledItems(stock_item) {
|
||||||
|
inventreeGet(
|
||||||
|
'{% url "api-stock-list" %}',
|
||||||
|
{
|
||||||
|
belongs_to: stock_item,
|
||||||
|
part_detail: true,
|
||||||
|
supplier_detail: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
// Add the returned stock items into the table
|
||||||
|
let row = table.bootstrapTable('getRowByUniqueId', stock_item);
|
||||||
|
row.installed_items_received = true;
|
||||||
|
|
||||||
|
table.bootstrapTable('updateByUniqueId', stock_item, row, true);
|
||||||
|
table.bootstrapTable('append', response);
|
||||||
|
|
||||||
|
// Auto-expand the newly added data
|
||||||
|
$(`.treegrid-${stock_item}`).treegrid('expand');
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
console.error(`Error requesting installed items for ${stock_item}`);
|
||||||
|
showApiError(xhr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent_id = 'top-level';
|
||||||
|
|
||||||
table.inventreeTable({
|
table.inventreeTable({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
formatNoMatches: function() {
|
formatNoMatches: function() {
|
||||||
@ -2177,6 +2228,35 @@ function loadStockTable(table, options) {
|
|||||||
showColumns: true,
|
showColumns: true,
|
||||||
showFooter: true,
|
showFooter: true,
|
||||||
columns: columns,
|
columns: columns,
|
||||||
|
treeEnable: show_installed_items,
|
||||||
|
rootParentId: parent_id,
|
||||||
|
parentIdField: 'belongs_to',
|
||||||
|
uniqueId: 'pk',
|
||||||
|
idField: 'pk',
|
||||||
|
treeShowField: 'part',
|
||||||
|
onPostBody: function() {
|
||||||
|
|
||||||
|
if (show_installed_items) {
|
||||||
|
table.treegrid({
|
||||||
|
treeColumn: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
table.treegrid('collapseAll');
|
||||||
|
|
||||||
|
// Callback for 'load sub-items' button
|
||||||
|
table.find('.load-sub-items').click(function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
let pk = $(this).attr('pk');
|
||||||
|
let row = table.bootstrapTable('getRowByUniqueId', pk);
|
||||||
|
|
||||||
|
requestInstalledItems(row.pk);
|
||||||
|
|
||||||
|
row.installed_items_requested = true;
|
||||||
|
table.bootstrapTable('updateByUniqueId', pk, row, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var buttons = [
|
var buttons = [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user