mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
BOM Table Improvements (#3310)
* Bug fix for "multi delete" form - Was requesting entire LIST endpoint before launching form - Could cause extremely long delays before window opened * Improve rendering of "no stock available" in BOM table * Adds footer row to BOM table - Display total number of parts - Display minimum "can build" amount * Added extra information to footer row * Annotate 'ordering' quantity to BOM list - Display this quantity in the BOM table * Bump API version * JS linting * Allow BOM list to be filtered by "on_order" parameter * Add information showing amount that can be built once orders are received
This commit is contained in:
parent
13dc14ce6f
commit
8f10bbb7e1
@ -2,11 +2,15 @@
|
|||||||
|
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 61
|
INVENTREE_API_VERSION = 64
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
Increment this API version number whenever there is a significant change to the API that any clients need to know about
|
||||||
|
|
||||||
|
v64 -> 2022-07-08 : https://github.com/inventree/InvenTree/pull/3310
|
||||||
|
- Annotate 'on_order' quantity to BOM list API
|
||||||
|
- Allow BOM List API endpoint to be filtered by "on_order" parameter
|
||||||
|
|
||||||
v63 -> 2022-07-06 : https://github.com/inventree/InvenTree/pull/3301
|
v63 -> 2022-07-06 : https://github.com/inventree/InvenTree/pull/3301
|
||||||
- Allow BOM List API endpoint to be filtered by "available_stock" paramater
|
- Allow BOM List API endpoint to be filtered by "available_stock" paramater
|
||||||
|
|
||||||
|
@ -1545,6 +1545,20 @@ class BomFilter(rest_filters.FilterSet):
|
|||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order")
|
||||||
|
|
||||||
|
def filter_on_order(self, queryset, name, value):
|
||||||
|
"""Filter the queryset based on whether each line item has any stock on order"""
|
||||||
|
|
||||||
|
value = str2bool(value)
|
||||||
|
|
||||||
|
if value:
|
||||||
|
queryset = queryset.filter(on_order__gt=0)
|
||||||
|
else:
|
||||||
|
queryset = queryset.filter(on_order=0)
|
||||||
|
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
class BomList(ListCreateDestroyAPIView):
|
class BomList(ListCreateDestroyAPIView):
|
||||||
"""API endpoint for accessing a list of BomItem objects.
|
"""API endpoint for accessing a list of BomItem objects.
|
||||||
|
@ -524,6 +524,8 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
purchase_price_range = serializers.SerializerMethodField()
|
purchase_price_range = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
on_order = serializers.FloatField(read_only=True)
|
||||||
|
|
||||||
# Annotated fields for available stock
|
# Annotated fields for available stock
|
||||||
available_stock = serializers.FloatField(read_only=True)
|
available_stock = serializers.FloatField(read_only=True)
|
||||||
available_substitute_stock = serializers.FloatField(read_only=True)
|
available_substitute_stock = serializers.FloatField(read_only=True)
|
||||||
@ -593,6 +595,11 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
ref = 'sub_part__'
|
ref = 'sub_part__'
|
||||||
|
|
||||||
|
# Annotate with the total "on order" amount for the sub-part
|
||||||
|
queryset = queryset.annotate(
|
||||||
|
on_order=part.filters.annotate_on_order_quantity(ref),
|
||||||
|
)
|
||||||
|
|
||||||
# Calculate "total stock" for the referenced sub_part
|
# Calculate "total stock" for the referenced sub_part
|
||||||
# Calculate the "build_order_allocations" for the sub_part
|
# Calculate the "build_order_allocations" for the sub_part
|
||||||
# Note that these fields are only aliased, not annotated
|
# Note that these fields are only aliased, not annotated
|
||||||
@ -719,6 +726,8 @@ class BomItemSerializer(InvenTreeModelSerializer):
|
|||||||
'available_substitute_stock',
|
'available_substitute_stock',
|
||||||
'available_variant_stock',
|
'available_variant_stock',
|
||||||
|
|
||||||
|
# Annotated field describing quantity on order
|
||||||
|
'on_order',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -297,6 +297,7 @@
|
|||||||
stock_item: {{ item.pk }},
|
stock_item: {{ item.pk }},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
multi_delete: true,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
title: '{% trans "Delete Test Data" %}',
|
title: '{% trans "Delete Test Data" %}',
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
|
@ -72,6 +72,7 @@ $('#history-delete').click(function() {
|
|||||||
'{% url "api-notifications-list" %}',
|
'{% url "api-notifications-list" %}',
|
||||||
{
|
{
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
multi_delete: true,
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
title: '{% trans "Delete Notifications" %}',
|
title: '{% trans "Delete Notifications" %}',
|
||||||
onSuccess: function() {
|
onSuccess: function() {
|
||||||
|
@ -109,6 +109,7 @@ function deleteAttachments(attachments, url, options={}) {
|
|||||||
|
|
||||||
constructForm(url, {
|
constructForm(url, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
multi_delete: true,
|
||||||
title: '{% trans "Delete Attachments" %}',
|
title: '{% trans "Delete Attachments" %}',
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
form_data: {
|
form_data: {
|
||||||
|
@ -651,9 +651,10 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Delete the selected BOM items from the database
|
||||||
|
*/
|
||||||
function deleteBomItems(items, options={}) {
|
function deleteBomItems(items, options={}) {
|
||||||
/* Delete the selected BOM items from the database
|
|
||||||
*/
|
|
||||||
|
|
||||||
function renderItem(item, opts={}) {
|
function renderItem(item, opts={}) {
|
||||||
|
|
||||||
@ -696,6 +697,7 @@ function deleteBomItems(items, options={}) {
|
|||||||
|
|
||||||
constructForm('{% url "api-bom-list" %}', {
|
constructForm('{% url "api-bom-list" %}', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
multi_delete: true,
|
||||||
title: '{% trans "Delete selected BOM items?" %}',
|
title: '{% trans "Delete selected BOM items?" %}',
|
||||||
form_data: {
|
form_data: {
|
||||||
items: ids,
|
items: ids,
|
||||||
@ -877,6 +879,32 @@ function loadBomTable(table, options={}) {
|
|||||||
|
|
||||||
return text;
|
return text;
|
||||||
},
|
},
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
|
||||||
|
// Top-level BOM count
|
||||||
|
var top_total = 0;
|
||||||
|
|
||||||
|
// Total BOM count
|
||||||
|
var all_total = 0;
|
||||||
|
|
||||||
|
data.forEach(function(row) {
|
||||||
|
var q = +row['quantity'] || 0;
|
||||||
|
|
||||||
|
all_total += q;
|
||||||
|
|
||||||
|
if (row.part == options.parent_id) {
|
||||||
|
top_total += q;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var total = `${top_total}`;
|
||||||
|
|
||||||
|
if (top_total != all_total) {
|
||||||
|
total += ` / ${all_total}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cols.push({
|
cols.push({
|
||||||
@ -897,9 +925,10 @@ function loadBomTable(table, options={}) {
|
|||||||
var text = `${available_stock}`;
|
var text = `${available_stock}`;
|
||||||
|
|
||||||
if (available_stock <= 0) {
|
if (available_stock <= 0) {
|
||||||
text = `<span class='badge rounded-pill bg-danger'>{% trans "No Stock Available" %}</span>`;
|
text += `<span class='fas fa-times-circle icon-red float-right' title='{% trans "No Stock Available" %}'></span>`;
|
||||||
} else {
|
} else {
|
||||||
var extra = '';
|
var extra = '';
|
||||||
|
|
||||||
if ((substitute_stock > 0) && (variant_stock > 0)) {
|
if ((substitute_stock > 0) && (variant_stock > 0)) {
|
||||||
extra = '{% trans "Includes variant and substitute stock" %}';
|
extra = '{% trans "Includes variant and substitute stock" %}';
|
||||||
} else if (variant_stock > 0) {
|
} else if (variant_stock > 0) {
|
||||||
@ -913,6 +942,10 @@ function loadBomTable(table, options={}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (row.on_order && row.on_order > 0) {
|
||||||
|
text += `<span class='fas fa-shopping-cart float-right' title='{% trans "On Order" %}: ${row.on_order}'></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
return renderLink(text, url);
|
return renderLink(text, url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -1010,7 +1043,36 @@ function loadBomTable(table, options={}) {
|
|||||||
can_build = available / row.quantity;
|
can_build = available / row.quantity;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatDecimal(can_build, 2);
|
var text = formatDecimal(can_build, 2);
|
||||||
|
|
||||||
|
// Take "on order" quantity into account
|
||||||
|
if (row.on_order && row.on_order > 0 && row.quantity > 0) {
|
||||||
|
available += row.on_order;
|
||||||
|
can_build = available / row.quantity;
|
||||||
|
|
||||||
|
text += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Including On Order" %}: ${can_build}'></span>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
},
|
||||||
|
footerFormatter: function(data) {
|
||||||
|
var can_build = null;
|
||||||
|
|
||||||
|
data.forEach(function(row) {
|
||||||
|
if (row.part == options.parent_id && row.quantity > 0) {
|
||||||
|
var cb = availableQuantity(row) / row.quantity;
|
||||||
|
|
||||||
|
if (can_build == null || cb < can_build) {
|
||||||
|
can_build = cb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (can_build == null) {
|
||||||
|
can_build = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return can_build;
|
||||||
},
|
},
|
||||||
sorter: function(valA, valB, rowA, rowB) {
|
sorter: function(valA, valB, rowA, rowB) {
|
||||||
// Function to sort the "can build" quantity
|
// Function to sort the "can build" quantity
|
||||||
@ -1131,6 +1193,7 @@ function loadBomTable(table, options={}) {
|
|||||||
parentIdField: 'parentId',
|
parentIdField: 'parentId',
|
||||||
treeShowField: 'sub_part',
|
treeShowField: 'sub_part',
|
||||||
showColumns: true,
|
showColumns: true,
|
||||||
|
showFooter: true,
|
||||||
name: 'bom',
|
name: 'bom',
|
||||||
sortable: true,
|
sortable: true,
|
||||||
search: true,
|
search: true,
|
||||||
|
@ -263,6 +263,7 @@ function deleteSupplierParts(parts, options={}) {
|
|||||||
|
|
||||||
constructForm('{% url "api-supplier-part-list" %}', {
|
constructForm('{% url "api-supplier-part-list" %}', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
multi_delete: true,
|
||||||
title: '{% trans "Delete Supplier Parts" %}',
|
title: '{% trans "Delete Supplier Parts" %}',
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
form_data: {
|
form_data: {
|
||||||
@ -491,6 +492,7 @@ function deleteManufacturerParts(selections, options={}) {
|
|||||||
|
|
||||||
constructForm('{% url "api-manufacturer-part-list" %}', {
|
constructForm('{% url "api-manufacturer-part-list" %}', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
multi_delete: true,
|
||||||
title: '{% trans "Delete Manufacturer Parts" %}',
|
title: '{% trans "Delete Manufacturer Parts" %}',
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
form_data: {
|
form_data: {
|
||||||
@ -538,6 +540,7 @@ function deleteManufacturerPartParameters(selections, options={}) {
|
|||||||
|
|
||||||
constructForm('{% url "api-manufacturer-part-parameter-list" %}', {
|
constructForm('{% url "api-manufacturer-part-parameter-list" %}', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
multi_delete: true,
|
||||||
title: '{% trans "Delete Parameters" %}',
|
title: '{% trans "Delete Parameters" %}',
|
||||||
preFormContent: html,
|
preFormContent: html,
|
||||||
form_data: {
|
form_data: {
|
||||||
|
@ -250,30 +250,40 @@ function constructChangeForm(fields, options) {
|
|||||||
*/
|
*/
|
||||||
function constructDeleteForm(fields, options) {
|
function constructDeleteForm(fields, options) {
|
||||||
|
|
||||||
// Request existing data from the API endpoint
|
// If we are deleting a specific "instance" (i.e. a single object)
|
||||||
// This data can be used to render some information on the form
|
// then we request the instance information first
|
||||||
$.ajax({
|
|
||||||
url: options.url,
|
|
||||||
type: 'GET',
|
|
||||||
contentType: 'application/json',
|
|
||||||
dataType: 'json',
|
|
||||||
accepts: {
|
|
||||||
json: 'application/json',
|
|
||||||
},
|
|
||||||
success: function(data) {
|
|
||||||
|
|
||||||
// Store the instance data
|
// However we may be performing a "multi-delete" (against a list endpoint),
|
||||||
options.instance = data;
|
// in which case we do not want to perform such a request!
|
||||||
|
|
||||||
constructFormBody(fields, options);
|
if (options.multi_delete) {
|
||||||
},
|
constructFormBody(fields, options);
|
||||||
error: function(xhr) {
|
} else {
|
||||||
// TODO: Handle error here
|
// Request existing data from the API endpoint
|
||||||
console.error(`Error in constructDeleteForm at '${options.url}`);
|
// This data can be used to render some information on the form
|
||||||
|
$.ajax({
|
||||||
|
url: options.url,
|
||||||
|
type: 'GET',
|
||||||
|
contentType: 'application/json',
|
||||||
|
dataType: 'json',
|
||||||
|
accepts: {
|
||||||
|
json: 'application/json',
|
||||||
|
},
|
||||||
|
success: function(data) {
|
||||||
|
|
||||||
showApiError(xhr, options.url);
|
// Store the instance data
|
||||||
}
|
options.instance = data;
|
||||||
});
|
|
||||||
|
constructFormBody(fields, options);
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
// TODO: Handle error here
|
||||||
|
console.error(`Error in constructDeleteForm at '${options.url}`);
|
||||||
|
|
||||||
|
showApiError(xhr, options.url);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,6 +63,10 @@ function getAvailableTableFilters(tableKey) {
|
|||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Has Available Stock" %}',
|
title: '{% trans "Has Available Stock" %}',
|
||||||
},
|
},
|
||||||
|
on_order: {
|
||||||
|
type: 'bool',
|
||||||
|
title: '{% trans "On Order" %}',
|
||||||
|
},
|
||||||
validated: {
|
validated: {
|
||||||
type: 'bool',
|
type: 'bool',
|
||||||
title: '{% trans "Validated" %}',
|
title: '{% trans "Validated" %}',
|
||||||
|
Loading…
Reference in New Issue
Block a user