mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 04:55:44 +00:00
Adds 'consumable' field to BomItem model (#2890)
* Adds 'consumable' field to BomItem model * Add consumable field to API * Consumable items always count as "allocated" for a build * Add new BOM fields to BOM item checksum calculation * Display 'consumable' status in BOM table * Fix order of database migrations * Update unit tests * Fix for BOM table * Remove "infinite" field from StockItem model - Not used anywhere for functionality - Hidden from the user - Now replaced by the "consumable" concept in the BuildOrder model * Update build order allocation table display * Prevent auto-allocation of stock to consumable BOM items * Ignore consumable BOM items when allocating stock to a build order * Reimplmement "footer" row for BOM table * Fix "can_build" calculation - Ignore "consumable" BOM items * Unrelated typo fix * Tweak BOM table * More visual tweaks to BOM table * Add unit test for consumable field
This commit is contained in:
@ -382,6 +382,7 @@ function bomItemFields() {
|
||||
note: {},
|
||||
allow_variants: {},
|
||||
inherited: {},
|
||||
consumable: {},
|
||||
optional: {},
|
||||
};
|
||||
|
||||
@ -761,7 +762,22 @@ function loadBomTable(table, options={}) {
|
||||
}
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
function canBuildQuantity(row) {
|
||||
// Calculate how many of each row we can make, given current stock
|
||||
|
||||
if (row.consumable) {
|
||||
// If the row is "consumable" we do not 'track' the quantity
|
||||
return Infinity;
|
||||
}
|
||||
|
||||
// Prevent div-by-zero or negative errors
|
||||
if ((row.quantity || 0) <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return availableQuantity(row) / row.quantity;
|
||||
}
|
||||
|
||||
// Construct the table columns
|
||||
@ -844,6 +860,9 @@ function loadBomTable(table, options={}) {
|
||||
{
|
||||
field: 'sub_part_detail.description',
|
||||
title: '{% trans "Description" %}',
|
||||
formatter: function(value) {
|
||||
return withTitle(shortenString(value), value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -872,8 +891,12 @@ function loadBomTable(table, options={}) {
|
||||
text += ` <small>${row.sub_part_detail.units}</small>`;
|
||||
}
|
||||
|
||||
if (row.consumable) {
|
||||
text += ` <small>({% trans "Consumable" %})</small>`;
|
||||
}
|
||||
|
||||
if (row.optional) {
|
||||
text += ' ({% trans "Optional" %})';
|
||||
text += ' <small>({% trans "Optional" %})</small>';
|
||||
}
|
||||
|
||||
if (row.overage) {
|
||||
@ -966,40 +989,11 @@ function loadBomTable(table, options={}) {
|
||||
if (row.substitutes && row.substitutes.length > 0) {
|
||||
return row.substitutes.length;
|
||||
} else {
|
||||
return `-`;
|
||||
return yesNoLabel(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (show_pricing) {
|
||||
cols.push({
|
||||
field: 'purchase_price_range',
|
||||
title: '{% trans "Purchase Price Range" %}',
|
||||
searchable: false,
|
||||
sortable: true,
|
||||
});
|
||||
|
||||
cols.push({
|
||||
field: 'purchase_price_avg',
|
||||
title: '{% trans "Purchase Price Average" %}',
|
||||
searchable: false,
|
||||
sortable: true,
|
||||
});
|
||||
|
||||
cols.push({
|
||||
field: 'price_range',
|
||||
title: '{% trans "Supplier Cost" %}',
|
||||
sortable: true,
|
||||
formatter: function(value) {
|
||||
if (value) {
|
||||
return value;
|
||||
} else {
|
||||
return `<span class='warning-msg'>{% trans 'No supplier pricing available' %}</span>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cols.push({
|
||||
field: 'optional',
|
||||
title: '{% trans "Optional" %}',
|
||||
@ -1009,6 +1003,15 @@ function loadBomTable(table, options={}) {
|
||||
}
|
||||
});
|
||||
|
||||
cols.push({
|
||||
field: 'consumable',
|
||||
title: '{% trans "Consumable" %}',
|
||||
searchable: false,
|
||||
formatter: function(value) {
|
||||
return yesNoLabel(value);
|
||||
}
|
||||
});
|
||||
|
||||
cols.push({
|
||||
field: 'allow_variants',
|
||||
title: '{% trans "Allow Variants" %}',
|
||||
@ -1037,36 +1040,63 @@ function loadBomTable(table, options={}) {
|
||||
}
|
||||
});
|
||||
|
||||
if (show_pricing) {
|
||||
cols.push({
|
||||
field: 'purchase_price_range',
|
||||
title: '{% trans "Purchase Price Range" %}',
|
||||
searchable: false,
|
||||
sortable: true,
|
||||
});
|
||||
|
||||
cols.push({
|
||||
field: 'purchase_price_avg',
|
||||
title: '{% trans "Purchase Price Average" %}',
|
||||
searchable: false,
|
||||
sortable: true,
|
||||
});
|
||||
|
||||
cols.push({
|
||||
field: 'price_range',
|
||||
title: '{% trans "Supplier Cost" %}',
|
||||
sortable: true,
|
||||
formatter: function(value) {
|
||||
if (value) {
|
||||
return value;
|
||||
} else {
|
||||
return `<span class='warning-msg'>{% trans 'No supplier pricing available' %}</span>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cols.push(
|
||||
{
|
||||
field: 'can_build',
|
||||
title: '{% trans "Can Build" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
var can_build = 0;
|
||||
|
||||
var available = availableQuantity(row);
|
||||
|
||||
if (row.quantity > 0) {
|
||||
can_build = available / row.quantity;
|
||||
// "Consumable" parts are not tracked in the build
|
||||
if (row.consumable) {
|
||||
return `<em>{% trans "Consumable item" %}</em>`;
|
||||
}
|
||||
|
||||
var text = formatDecimal(can_build, 2);
|
||||
var can_build = canBuildQuantity(row);
|
||||
|
||||
// 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;
|
||||
return +can_build.toFixed(2);
|
||||
},
|
||||
sorter: function(valA, valB, rowA, rowB) {
|
||||
// Function to sort the "can build" quantity
|
||||
var cb_a = canBuildQuantity(rowA);
|
||||
var cb_b = canBuildQuantity(rowB);
|
||||
|
||||
text += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Including On Order" %}: ${formatDecimal(can_build, 2)}'></span>`;
|
||||
}
|
||||
|
||||
return text;
|
||||
return (cb_a > cb_b) ? 1 : -1;
|
||||
},
|
||||
footerFormatter: function(data) {
|
||||
var can_build = null;
|
||||
|
||||
data.forEach(function(row) {
|
||||
if (row.part == options.parent_id && row.quantity > 0) {
|
||||
if (row.quantity > 0 && !row.consumable) {
|
||||
var cb = availableQuantity(row) / row.quantity;
|
||||
|
||||
if (can_build == null || cb < can_build) {
|
||||
@ -1080,23 +1110,7 @@ function loadBomTable(table, options={}) {
|
||||
} else {
|
||||
return formatDecimal(can_build, 2);
|
||||
}
|
||||
},
|
||||
sorter: function(valA, valB, rowA, rowB) {
|
||||
// Function to sort the "can build" quantity
|
||||
var cb_a = 0;
|
||||
var cb_b = 0;
|
||||
|
||||
if (rowA.quantity > 0) {
|
||||
cb_a = availableQuantity(rowA) / rowA.quantity;
|
||||
}
|
||||
|
||||
if (rowB.quantity > 0) {
|
||||
cb_b = availableQuantity(rowB) / rowB.quantity;
|
||||
}
|
||||
|
||||
return (cb_a > cb_b) ? 1 : -1;
|
||||
},
|
||||
sortable: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@ -1107,6 +1121,9 @@ function loadBomTable(table, options={}) {
|
||||
title: '{% trans "Notes" %}',
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
formatter: function(value) {
|
||||
return withTitle(shortenString(value), value);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -1859,33 +1859,37 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
var icons = '';
|
||||
|
||||
if (available_stock < (required - allocated)) {
|
||||
icons += `<span class='fas fa-times-circle icon-red float-right' title='{% trans "Insufficient stock available" %}'></span>`;
|
||||
if (row.consumable) {
|
||||
icons += `<span class='fas fa-info-circle icon-blue float-right' title='{% trans "Consumable item" %}'></span>`;
|
||||
} else {
|
||||
icons += `<span class='fas fa-check-circle icon-green float-right' title='{% trans "Sufficient stock available" %}'></span>`;
|
||||
if (available_stock < (required - allocated)) {
|
||||
icons += `<span class='fas fa-times-circle icon-red float-right' title='{% trans "Insufficient stock available" %}'></span>`;
|
||||
} else {
|
||||
icons += `<span class='fas fa-check-circle icon-green float-right' title='{% trans "Sufficient stock available" %}'></span>`;
|
||||
}
|
||||
|
||||
if (available_stock <= 0) {
|
||||
icons += `<span class='badge rounded-pill bg-danger'>{% trans "No Stock Available" %}</span>`;
|
||||
} else {
|
||||
var extra = '';
|
||||
if ((substitute_stock > 0) && (variant_stock > 0)) {
|
||||
extra = '{% trans "Includes variant and substitute stock" %}';
|
||||
} else if (variant_stock > 0) {
|
||||
extra = '{% trans "Includes variant stock" %}';
|
||||
} else if (substitute_stock > 0) {
|
||||
extra = '{% trans "Includes substitute stock" %}';
|
||||
}
|
||||
|
||||
if (extra) {
|
||||
icons += `<span title='${extra}' class='fas fa-info-circle float-right icon-blue'></span>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (row.on_order && row.on_order > 0) {
|
||||
icons += `<span class='fas fa-shopping-cart float-right' title='{% trans "On Order" %}: ${row.on_order}'></span>`;
|
||||
}
|
||||
|
||||
if (available_stock <= 0) {
|
||||
icons += `<span class='badge rounded-pill bg-danger'>{% trans "No Stock Available" %}</span>`;
|
||||
} else {
|
||||
var extra = '';
|
||||
if ((substitute_stock > 0) && (variant_stock > 0)) {
|
||||
extra = '{% trans "Includes variant and substitute stock" %}';
|
||||
} else if (variant_stock > 0) {
|
||||
extra = '{% trans "Includes variant stock" %}';
|
||||
} else if (substitute_stock > 0) {
|
||||
extra = '{% trans "Includes substitute stock" %}';
|
||||
}
|
||||
|
||||
if (extra) {
|
||||
icons += `<span title='${extra}' class='fas fa-info-circle float-right icon-blue'></span>`;
|
||||
}
|
||||
}
|
||||
|
||||
return renderLink(text, url) + icons;
|
||||
},
|
||||
sorter: function(valA, valB, rowA, rowB) {
|
||||
@ -1898,8 +1902,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
title: '{% trans "Allocated" %}',
|
||||
sortable: true,
|
||||
formatter: function(value, row) {
|
||||
var allocated = allocatedQuantity(row);
|
||||
var required = requiredQuantity(row);
|
||||
var allocated = row.consumable ? required : allocatedQuantity(row);
|
||||
return makeProgressBar(allocated, required);
|
||||
},
|
||||
sorter: function(valA, valB, rowA, rowB) {
|
||||
@ -1938,6 +1942,11 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
field: 'actions',
|
||||
title: '{% trans "Actions" %}',
|
||||
formatter: function(value, row) {
|
||||
|
||||
if (row.consumable) {
|
||||
return `<em>{% trans "Consumable item" %}</em>`;
|
||||
}
|
||||
|
||||
// Generate action buttons for this build output
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
@ -2093,6 +2102,11 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
||||
for (var idx = 0; idx < bom_items.length; idx++) {
|
||||
var bom_item = bom_items[idx];
|
||||
|
||||
// Ignore "consumable" BOM items
|
||||
if (bom_item.consumable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var required = bom_item.required || 0;
|
||||
var allocated = bom_item.allocated || 0;
|
||||
var remaining = required - allocated;
|
||||
|
@ -79,6 +79,14 @@ function getAvailableTableFilters(tableKey) {
|
||||
type: 'bool',
|
||||
title: '{% trans "Allow Variant Stock" %}',
|
||||
},
|
||||
optional: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Optional" %}',
|
||||
},
|
||||
consumable: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Consumable" %}',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user