2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-28 03:26:45 +00:00

1825 lines
55 KiB
JavaScript

{% load i18n %}
{% load inventree_extras %}
/* globals
buildStatusDisplay,
constructForm,
global_settings,
imageHoverIcon,
inventreeGet,
launchModalForm,
linkButtonsToSelection,
loadTableFilters,
makeIconBadge,
makeIconButton,
makePartIcons,
makeProgressBar,
renderLink,
setupFilterList,
*/
/* exported
allocateStockToBuild,
editBuildOrder,
loadAllocationTable,
loadBuildOrderAllocationTable,
loadBuildOutputAllocationTable,
loadBuildOutputTable,
loadBuildTable,
*/
function buildFormFields() {
return {
reference: {
prefix: global_settings.BUILDORDER_REFERENCE_PREFIX,
},
part: {
filters: {
assembly: true,
virtual: false,
}
},
title: {},
quantity: {},
parent: {
filters: {
part_detail: true,
}
},
sales_order: {
icon: 'fa-truck',
},
batch: {},
target_date: {
icon: 'fa-calendar-alt',
},
take_from: {
icon: 'fa-sitemap',
},
destination: {
icon: 'fa-sitemap',
},
link: {
icon: 'fa-link',
},
issued_by: {
icon: 'fa-user',
},
responsible: {
icon: 'fa-users',
},
};
}
function editBuildOrder(pk) {
var fields = buildFormFields();
constructForm(`/api/build/${pk}/`, {
fields: fields,
reload: true,
title: '{% trans "Edit Build Order" %}',
});
}
function newBuildOrder(options={}) {
/* Launch modal form to create a new BuildOrder.
*/
var fields = buildFormFields();
// Specify the target part
if (options.part) {
fields.part.value = options.part;
}
// Specify the desired quantity
if (options.quantity) {
fields.quantity.value = options.quantity;
}
// Specify the parent build order
if (options.parent) {
fields.parent.value = options.parent;
}
// Specify a parent sales order
if (options.sales_order) {
fields.sales_order.value = options.sales_order;
}
constructForm(`/api/build/`, {
fields: fields,
follow: true,
method: 'POST',
title: '{% trans "Create Build Order" %}',
onSuccess: options.onSuccess,
});
}
/*
* Construct a set of output buttons for a particular build output
*/
function makeBuildOutputButtons(output_id, build_info, options={}) {
var html = `<div class='btn-group float-right' role='group'>`;
// Tracked parts? Must be individually allocated
if (build_info.tracked_parts) {
// Add a button to allocate stock against this build output
html += makeIconButton(
'fa-sign-in-alt icon-blue',
'button-output-allocate',
output_id,
'{% trans "Allocate stock items to this build output" %}',
{
disabled: true,
}
);
// Add a button to unallocate stock from this build output
html += makeIconButton(
'fa-minus-circle icon-red',
'button-output-unallocate',
output_id,
'{% trans "Unallocate stock from build output" %}',
);
}
// Add a button to "complete" this build output
html += makeIconButton(
'fa-check-circle icon-green',
'button-output-complete',
output_id,
'{% trans "Complete build output" %}',
);
// Add a button to "delete" this build output
html += makeIconButton(
'fa-trash-alt icon-red',
'button-output-delete',
output_id,
'{% trans "Delete build output" %}',
);
html += `</div>`;
return html;
}
/*
* Unallocate stock against a particular build order
*
* Options:
* - output: pk value for a stock item "build output"
* - bom_item: pk value for a particular BOMItem (build item)
*/
function unallocateStock(build_id, options={}) {
var url = `/api/build/${build_id}/unallocate/`;
var html = `
<div class='alert alert-block alert-warning'>
{% trans "Are you sure you wish to unallocate stock items from this build?" %}
</dvi>
`;
constructForm(url, {
method: 'POST',
confirm: true,
preFormContent: html,
fields: {
output: {
hidden: true,
value: options.output,
},
bom_item: {
hidden: true,
value: options.bom_item,
},
},
title: '{% trans "Unallocate Stock Items" %}',
onSuccess: function(response, opts) {
if (options.table) {
// Reload the parent table
$(options.table).bootstrapTable('refresh');
}
}
});
}
/**
* Launch a modal form to complete selected build outputs
*/
function completeBuildOutputs(build_id, outputs, options={}) {
if (outputs.length == 0) {
showAlertDialog(
'{% trans "Select Build Outputs" %}',
'{% trans "At least one build output must be selected" %}',
);
return;
}
// Render a single build output (StockItem)
function renderBuildOutput(output, opts={}) {
var pk = output.pk;
var output_html = imageHoverIcon(output.part_detail.thumbnail);
if (output.quantity == 1 && output.serial) {
output_html += `{% trans "Serial Number" %}: ${output.serial}`;
} else {
output_html += `{% trans "Quantity" %}: ${output.quantity}`;
}
var buttons = `<div class='btn-group float-right' role='group'>`;
buttons += makeIconButton('fa-times icon-red', 'button-row-remove', pk, '{% trans "Remove row" %}');
buttons += '</div>';
var field = constructField(
`outputs_output_${pk}`,
{
type: 'raw',
html: output_html,
},
{
hideLabels: true,
}
);
var html = `
<tr id='output_row_${pk}'>
<td>${field}</td>
<td>${output.part_detail.full_name}</td>
<td>${buttons}</td>
</tr>`;
return html;
}
// Construct table entries
var table_entries = '';
outputs.forEach(function(output) {
table_entries += renderBuildOutput(output);
});
var html = `
<table class='table table-striped table-condensed' id='build-complete-table'>
<thead>
<th colspan='2'>{% trans "Output" %}</th>
<th><!-- Actions --></th>
</thead>
<tbody>
${table_entries}
</tbody>
</table>`;
constructForm(`/api/build/${build_id}/complete/`, {
method: 'POST',
preFormContent: html,
fields: {
status: {},
location: {},
},
confirm: true,
title: '{% trans "Complete Build Outputs" %}',
afterRender: function(fields, opts) {
// Setup callbacks to remove outputs
$(opts.modal).find('.button-row-remove').click(function() {
var pk = $(this).attr('pk');
$(opts.modal).find(`#output_row_${pk}`).remove();
});
},
onSubmit: function(fields, opts) {
// Extract data elements from the form
var data = {
outputs: [],
status: getFormFieldValue('status', {}, opts),
location: getFormFieldValue('location', {}, opts),
};
var output_pk_values = [];
outputs.forEach(function(output) {
var pk = output.pk;
var row = $(opts.modal).find(`#output_row_${pk}`);
if (row.exists()) {
data.outputs.push({
output: pk,
});
output_pk_values.push(pk);
}
});
// Provide list of nested values
opts.nested = {
'outputs': output_pk_values,
};
inventreePut(
opts.url,
data,
{
method: 'POST',
success: function(response) {
// Hide the modal
$(opts.modal).modal('hide');
if (options.success) {
options.success(response);
}
},
error: function(xhr) {
switch (xhr.status) {
case 400:
handleFormErrors(xhr.responseJSON, fields, opts);
break;
default:
$(opts.modal).modal('hide');
showApiError(xhr, opts.url);
break;
}
}
}
);
}
});
}
/**
* Load a table showing all the BuildOrder allocations for a given part
*/
function loadBuildOrderAllocationTable(table, options={}) {
options.params['part_detail'] = true;
options.params['build_detail'] = true;
options.params['location_detail'] = true;
var filters = loadTableFilters('buildorderallocation');
for (var key in options.params) {
filters[key] = options.params[key];
}
setupFilterList('buildorderallocation', $(table));
$(table).inventreeTable({
url: '{% url "api-build-item-list" %}',
queryParams: filters,
name: 'buildorderallocation',
groupBy: false,
search: false,
paginationVAlign: 'bottom',
original: options.params,
formatNoMatches: function() {
return '{% trans "No build order allocations found" %}';
},
columns: [
{
field: 'pk',
visible: false,
switchable: false,
},
{
field: 'build',
switchable: false,
title: '{% trans "Build Order" %}',
formatter: function(value, row) {
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
var ref = `${prefix}${row.build_detail.reference}`;
return renderLink(ref, `/build/${row.build}/`);
}
},
{
field: 'item',
title: '{% trans "Stock Item" %}',
formatter: function(value, row) {
// Render a link to the particular stock item
var link = `/stock/item/${row.stock_item}/`;
var text = `{% trans "Stock Item" %} ${row.stock_item}`;
return renderLink(text, link);
}
},
{
field: 'location',
title: '{% trans "Location" %}',
formatter: function(value, row) {
if (!value) {
return '{% trans "Location not specified" %}';
}
var link = `/stock/location/${value}`;
var text = row.location_detail.description;
return renderLink(text, link);
}
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
sortable: true,
}
]
});
}
/*
* Display a "build output" table for a particular build.
*
* This displays a list of "active" (i.e. "in production") build outputs for a given build
*
*/
function loadBuildOutputTable(build_info, options={}) {
var table = options.table || '#build-output-table';
var params = options.params || {};
// Mandatory query filters
params.part_detail = true;
params.is_building = true;
params.build = build_info.pk;
// Construct a list of "tracked" BOM items
var tracked_bom_items = [];
var has_tracked_items = false;
build_info.bom_items.forEach(function(bom_item) {
if (bom_item.sub_part_detail.trackable) {
tracked_bom_items.push(bom_item);
has_tracked_items = true;
};
});
var filters = {};
for (var key in params) {
filters[key] = params[key];
}
// TODO: Initialize filter list
function setupBuildOutputButtonCallbacks() {
// Callback for the "allocate" button
$(table).find('.button-output-allocate').click(function() {
var pk = $(this).attr('pk');
// Find the "allocation" sub-table associated with this output
var subtable = $(`#output-sub-table-${pk}`);
if (subtable.exists()) {
var rows = subtable.bootstrapTable('getSelections');
// None selected? Use all!
if (rows.length == 0) {
rows = subtable.bootstrapTable('getData');
}
allocateStockToBuild(
build_info.pk,
build_info.part,
rows,
{
output: pk,
success: function() {
$(table).bootstrapTable('refresh');
}
}
);
} else {
console.log(`WARNING: Could not locate sub-table for output ${pk}`);
}
});
// Callack for the "unallocate" button
$(table).find('.button-output-unallocate').click(function() {
var pk = $(this).attr('pk');
unallocateStock(build_info.pk, {
output: pk,
table: table
});
});
// Callback for the "complete" button
$(table).find('.button-output-complete').click(function() {
var pk = $(this).attr('pk');
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
completeBuildOutputs(
build_info.pk,
[
output,
],
{
success: function() {
$(table).bootstrapTable('refresh');
}
}
);
});
// Callback for the "delete" button
$(table).find('.button-output-delete').click(function() {
var pk = $(this).attr('pk');
// TODO: Move this to the API
launchModalForm(
`/build/${build_info.pk}/delete-output/`,
{
data: {
output: pk
},
onSuccess: function() {
$(table).bootstrapTable('refresh');
}
}
);
});
}
/*
* Construct a "sub table" showing the required BOM items
*/
function constructBuildOutputSubTable(index, row, element) {
var sub_table_id = `output-sub-table-${row.pk}`;
var html = `
<div class='sub-table'>
<table class='table table-striped table-condensed' id='${sub_table_id}'></table>
</div>
`;
element.html(html);
loadBuildOutputAllocationTable(
build_info,
row,
{
table: `#${sub_table_id}`,
parent_table: table,
}
);
}
$(table).inventreeTable({
url: '{% url "api-stock-list" %}',
queryParams: filters,
original: params,
showColumns: false,
uniqueId: 'pk',
name: 'build-outputs',
sortable: true,
search: false,
sidePagination: 'server',
detailView: has_tracked_items,
detailFilter: function(index, row) {
return true;
},
detailFormatter: function(index, row, element) {
constructBuildOutputSubTable(index, row, element);
},
formatNoMatches: function() {
return '{% trans "No active build outputs found" %}';
},
onPostBody: function() {
// Add callbacks for the buttons
setupBuildOutputButtonCallbacks();
$(table).bootstrapTable('expandAllRows');
},
columns: [
{
title: '',
visible: true,
checkbox: true,
switchable: false,
},
{
field: 'part',
title: '{% trans "Part" %}',
formatter: function(value, row) {
var thumb = row.part_detail.thumbnail;
return imageHoverIcon(thumb) + row.part_detail.full_name + makePartIcons(row.part_detail);
}
},
{
field: 'quantity',
title: '{% trans "Quantity" %}',
formatter: function(value, row) {
var url = `/stock/item/${row.pk}/`;
var text = '';
if (row.serial && row.quantity == 1) {
text = `{% trans "Serial Number" %}: ${row.serial}`;
} else {
text = `{% trans "Quantity" %}: ${row.quantity}`;
}
return renderLink(text, url);
}
},
{
field: 'allocated',
title: '{% trans "Allocated Parts" %}',
visible: has_tracked_items,
formatter: function(value, row) {
return `<div id='output-progress-${row.pk}'><span class='fas fa-spin fa-spinner'></span></div>`;
}
},
{
field: 'actions',
title: '',
switchable: false,
formatter: function(value, row) {
return makeBuildOutputButtons(
row.pk,
build_info,
);
}
}
]
});
// Enable the "allocate" button when the sub-table is exanded
$(table).on('expand-row.bs.table', function(detail, index, row) {
$(`#button-output-allocate-${row.pk}`).prop('disabled', false);
});
// Disable the "allocate" button when the sub-table is collapsed
$(table).on('collapse-row.bs.table', function(detail, index, row) {
$(`#button-output-allocate-${row.pk}`).prop('disabled', true);
});
}
/*
* Display the "allocation table" for a particular build output.
*
* This displays a table of required allocations for a particular build output
*
* Args:
* - buildId: The PK of the Build object
* - partId: The PK of the Part object
* - output: The StockItem object which is the "output" of the build
* - options:
* -- table: The #id of the table (will be auto-calculated if not provided)
*/
function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
var buildId = buildInfo.pk;
var partId = buildInfo.part;
var outputId = null;
if (output) {
outputId = output.pk;
} else {
outputId = 'untracked';
}
var table = options.table;
if (options.table == null) {
table = `#allocation-table-${outputId}`;
}
// Filters
var filters = loadTableFilters('builditems');
var params = options.params || {};
for (var key in params) {
filters[key] = params[key];
}
setupFilterList('builditems', $(table), options.filterTarget || null);
// If an "output" is specified, then only "trackable" parts are allocated
// Otherwise, only "untrackable" parts are allowed
var trackable = ! !output;
function reloadTable() {
// Reload the entire build allocation table
$(table).bootstrapTable('refresh');
}
function requiredQuantity(row) {
// Return the requied quantity for a given row
var quantity = 0;
if (output) {
// "Tracked" parts are calculated against individual build outputs
quantity = row.quantity * output.quantity;
} else {
// "Untracked" parts are specified against the build itself
quantity = row.quantity * buildInfo.quantity;
}
// Store the required quantity in the row data
row.required = quantity;
return quantity;
}
function sumAllocations(row) {
// Calculat total allocations for a given row
if (!row.allocations) {
row.allocated = 0;
return 0;
}
var quantity = 0;
row.allocations.forEach(function(item) {
quantity += item.quantity;
});
row.allocated = quantity;
return quantity;
}
function setupCallbacks() {
// Register button callbacks once table data are loaded
// Callback for 'allocate' button
$(table).find('.button-add').click(function() {
// Primary key of the 'sub_part'
var pk = $(this).attr('pk');
// Extract BomItem information from this row
var row = $(table).bootstrapTable('getRowByUniqueId', pk);
if (!row) {
console.log('WARNING: getRowByUniqueId returned null');
return;
}
allocateStockToBuild(
buildId,
partId,
[
row,
],
{
source_location: buildInfo.source_location,
success: function(data) {
$(table).bootstrapTable('refresh');
},
output: output == null ? null : output.pk,
}
);
});
// Callback for 'buy' button
$(table).find('.button-buy').click(function() {
var pk = $(this).attr('pk');
launchModalForm('{% url "order-parts" %}', {
data: {
parts: [
pk,
]
}
});
});
// Callback for 'build' button
$(table).find('.button-build').click(function() {
var pk = $(this).attr('pk');
// Extract row data from the table
var idx = $(this).closest('tr').attr('data-index');
var row = $(table).bootstrapTable('getData')[idx];
newBuildOrder({
part: pk,
parent: buildId,
quantity: requiredQuantity(row) - sumAllocations(row),
});
});
// Callback for 'unallocate' button
$(table).find('.button-unallocate').click(function() {
// Extract row data from the table
var idx = $(this).closest('tr').attr('data-index');
var row = $(table).bootstrapTable('getData')[idx];
unallocateStock(buildId, {
bom_item: row.pk,
output: outputId == 'untracked' ? null : outputId,
table: table,
});
});
}
// Load table of BOM items
$(table).inventreeTable({
url: '{% url "api-bom-list" %}',
queryParams: {
part: partId,
sub_part_detail: true,
sub_part_trackable: trackable,
},
disablePagination: true,
formatNoMatches: function() {
return '{% trans "No BOM items found" %}';
},
name: 'build-allocation',
uniqueId: 'sub_part',
search: options.search || false,
onPostBody: function(data) {
// Setup button callbacks
setupCallbacks();
},
onLoadSuccess: function(tableData) {
// Once the BOM data are loaded, request allocation data for this build output
var params = {
build: buildId,
part_detail: true,
location_detail: true,
};
if (output) {
params.sub_part_trackable = true;
params.output = outputId;
} else {
params.sub_part_trackable = false;
}
inventreeGet('/api/build/item/',
params,
{
success: function(data) {
// Iterate through the returned data, and group by the part they point to
var allocations = {};
// Total number of line items
var totalLines = tableData.length;
// Total number of "completely allocated" lines
var allocatedLines = 0;
data.forEach(function(item) {
// Group BuildItem objects by part
var part = item.bom_part || item.part;
var key = parseInt(part);
if (!(key in allocations)) {
allocations[key] = [];
}
allocations[key].push(item);
});
// Now update the allocations for each row in the table
for (var key in allocations) {
// Select the associated row in the table
var tableRow = $(table).bootstrapTable('getRowByUniqueId', key);
if (!tableRow) {
continue;
}
// Set the allocation list for that row
tableRow.allocations = allocations[key];
// Calculate the total allocated quantity
var allocatedQuantity = sumAllocations(tableRow);
var requiredQuantity = 0;
if (output) {
requiredQuantity = tableRow.quantity * output.quantity;
} else {
requiredQuantity = tableRow.quantity * buildInfo.quantity;
}
// Is this line item fully allocated?
if (allocatedQuantity >= requiredQuantity) {
allocatedLines += 1;
}
// Push the updated row back into the main table
$(table).bootstrapTable('updateByUniqueId', key, tableRow, true);
}
// Update the progress bar for this build output
var build_progress = $(`#output-progress-${outputId}`);
if (build_progress.exists()) {
if (totalLines > 0) {
var progress = makeProgressBar(
allocatedLines,
totalLines,
{
max_width: '150px',
}
);
build_progress.html(progress);
} else {
build_progress.html('');
}
} else {
console.log(`WARNING: Could not find progress bar for output ${outputId}`);
}
}
}
);
},
sortable: true,
showColumns: false,
detailView: true,
detailFilter: function(index, row) {
return row.allocations != null;
},
detailFormatter: function(index, row, element) {
// Contruct an 'inner table' which shows which stock items have been allocated
var subTableId = `allocation-table-${row.pk}`;
var html = `<div class='sub-table'><table class='table table-condensed table-striped' id='${subTableId}'></table></div>`;
element.html(html);
var subTable = $(`#${subTableId}`);
subTable.bootstrapTable({
data: row.allocations,
showHeader: true,
columns: [
{
field: 'part',
title: '{% trans "Part" %}',
formatter: function(value, row) {
var html = imageHoverIcon(row.part_detail.thumbnail);
html += renderLink(row.part_detail.full_name, `/part/${value}/`);
return html;
}
},
{
width: '50%',
field: 'quantity',
title: '{% trans "Assigned Stock" %}',
formatter: function(value, row) {
var text = '';
var url = '';
var serial = row.serial;
if (row.stock_item_detail) {
serial = row.stock_item_detail.serial;
}
if (serial && row.quantity == 1) {
text = `{% trans "Serial Number" %}: ${serial}`;
} else {
text = `{% trans "Quantity" %}: ${row.quantity}`;
}
var pk = row.stock_item || row.pk;
url = `/stock/item/${pk}/`;
return renderLink(text, url);
}
},
{
field: 'location',
title: '{% trans "Location" %}',
formatter: function(value, row) {
if (row.stock_item_detail.location) {
var text = row.stock_item_detail.location_name;
var url = `/stock/location/${row.stock_item_detail.location}/`;
return renderLink(text, url);
} else {
return '<i>{% trans "No location set" %}</i>';
}
}
},
{
field: 'actions',
formatter: function(value, row) {
/* Actions available for a particular stock item allocation:
*
* - Edit the allocation quantity
* - Delete the allocation
*/
var pk = row.pk;
var html = `<div class='btn-group float-right' role='group'>`;
html += makeIconButton('fa-edit icon-blue', 'button-allocation-edit', pk, '{% trans "Edit stock allocation" %}');
html += makeIconButton('fa-trash-alt icon-red', 'button-allocation-delete', pk, '{% trans "Delete stock allocation" %}');
html += `</div>`;
return html;
}
}
]
});
// Assign button callbacks to the newly created allocation buttons
subTable.find('.button-allocation-edit').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/build/item/${pk}/`, {
fields: {
quantity: {},
},
title: '{% trans "Edit Allocation" %}',
onSuccess: reloadTable,
});
});
subTable.find('.button-allocation-delete').click(function() {
var pk = $(this).attr('pk');
constructForm(`/api/build/item/${pk}/`, {
method: 'DELETE',
title: '{% trans "Remove Allocation" %}',
onSuccess: reloadTable,
});
});
},
columns: [
{
visible: true,
switchable: false,
checkbox: true,
},
{
field: 'sub_part_detail.full_name',
title: '{% trans "Required Part" %}',
sortable: true,
formatter: function(value, row) {
var url = `/part/${row.sub_part}/`;
var thumb = row.sub_part_detail.thumbnail;
var name = row.sub_part_detail.full_name;
var html = imageHoverIcon(thumb) + renderLink(name, url);
html += makePartIcons(row.sub_part_detail);
if (row.substitutes && row.substitutes.length > 0) {
html += makeIconBadge('fa-exchange-alt', '{% trans "Substitute parts available" %}');
}
if (row.allow_variants) {
html += makeIconBadge('fa-sitemap', '{% trans "Variant stock allowed" %}');
}
return html;
}
},
{
field: 'reference',
title: '{% trans "Reference" %}',
sortable: true,
},
{
field: 'quantity',
title: '{% trans "Quantity Per" %}',
sortable: true,
},
{
field: 'sub_part_detail.stock',
title: '{% trans "Available" %}',
sortable: true,
},
{
field: 'allocated',
title: '{% trans "Allocated" %}',
sortable: true,
formatter: function(value, row) {
var allocated = 0;
if (row.allocations) {
row.allocations.forEach(function(item) {
allocated += item.quantity;
});
}
var required = requiredQuantity(row);
return makeProgressBar(allocated, required);
},
sorter: function(valA, valB, rowA, rowB) {
// Custom sorting function for progress bars
var aA = sumAllocations(rowA);
var aB = sumAllocations(rowB);
var qA = requiredQuantity(rowA);
var qB = requiredQuantity(rowB);
// Handle the case where both numerators are zero
if ((aA == 0) && (aB == 0)) {
return (qA > qB) ? 1 : -1;
}
// Handle the case where either denominator is zero
if ((qA == 0) || (qB == 0)) {
return 1;
}
var progressA = parseFloat(aA) / qA;
var progressB = parseFloat(aB) / qB;
// Handle the case where both ratios are equal
if (progressA == progressB) {
return (qA < qB) ? 1 : -1;
}
if (progressA == progressB) return 0;
return (progressA < progressB) ? 1 : -1;
}
},
{
field: 'actions',
title: '{% trans "Actions" %}',
formatter: function(value, row) {
// Generate action buttons for this build output
var html = `<div class='btn-group float-right' role='group'>`;
if (sumAllocations(row) < requiredQuantity(row)) {
if (row.sub_part_detail.assembly) {
html += makeIconButton('fa-tools icon-blue', 'button-build', row.sub_part, '{% trans "Build stock" %}');
}
if (row.sub_part_detail.purchaseable) {
html += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', row.sub_part, '{% trans "Order stock" %}');
}
html += makeIconButton('fa-sign-in-alt icon-green', 'button-add', row.sub_part, '{% trans "Allocate stock" %}');
}
html += makeIconButton(
'fa-minus-circle icon-red', 'button-unallocate', row.sub_part,
'{% trans "Unallocate stock" %}',
{
disabled: row.allocations == null
}
);
html += '</div>';
return html;
}
},
]
});
}
/**
* Allocate stock items to a build
*
* arguments:
* - buildId: ID / PK value for the build
* - partId: ID / PK value for the part being built
* - bom_items: A list of BomItem objects to be allocated
*
* options:
* - output: ID / PK of the associated build output (or null for untracked items)
* - source_location: ID / PK of the top-level StockLocation to source stock from (or null)
*/
function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
// ID of the associated "build output" (or null)
var output_id = options.output || null;
var source_location = options.source_location;
function renderBomItemRow(bom_item, quantity) {
var pk = bom_item.pk;
var sub_part = bom_item.sub_part_detail;
var thumb = thumbnailImage(bom_item.sub_part_detail.thumbnail);
var delete_button = `<div class='btn-group float-right' role='group'>`;
delete_button += makeIconButton(
'fa-times icon-red',
'button-row-remove',
pk,
'{% trans "Remove row" %}',
);
delete_button += `</div>`;
var quantity_input = constructField(
`items_quantity_${pk}`,
{
type: 'decimal',
min_value: 0,
value: quantity || 0,
title: '{% trans "Specify stock allocation quantity" %}',
required: true,
},
{
hideLabels: true,
}
);
var allocated_display = makeProgressBar(
bom_item.allocated,
bom_item.required,
);
var stock_input = constructField(
`items_stock_item_${pk}`,
{
type: 'related field',
required: 'true',
},
{
hideLabels: true,
}
);
// var stock_input = constructRelatedFieldInput(`items_stock_item_${pk}`);
var html = `
<tr id='allocation_row_${pk}' class='part-allocation-row'>
<td id='part_${pk}'>
${thumb} ${sub_part.full_name}
</td>
<td id='allocated_${pk}'>
${allocated_display}
</td>
<td id='stock_item_${pk}'>
${stock_input}
</td>
<td id='quantity_${pk}'>
${quantity_input}
</td>
<td id='buttons_${pk}'>
${delete_button}
</td>
</tr>
`;
return html;
}
var table_entries = '';
for (var idx = 0; idx < bom_items.length; idx++) {
var bom_item = bom_items[idx];
var required = bom_item.required || 0;
var allocated = bom_item.allocated || 0;
var remaining = required - allocated;
if (remaining < 0) {
remaining = 0;
}
// We only care about entries which are not yet fully allocated
if (remaining > 0) {
table_entries += renderBomItemRow(bom_item, remaining);
}
}
if (table_entries.length == 0) {
showAlertDialog(
'{% trans "Select Parts" %}',
'{% trans "You must select at least one part to allocate" %}',
);
return;
}
var html = ``;
// Render a "source location" input
html += constructField(
'take_from',
{
type: 'related field',
label: '{% trans "Source Location" %}',
help_text: '{% trans "Select source location (leave blank to take from all locations)" %}',
required: false,
},
{},
);
// Create table of parts
html += `
<table class='table table-striped table-condensed' id='stock-allocation-table'>
<thead>
<tr>
<th>{% trans "Part" %}</th>
<th>{% trans "Allocated" %}</th>
<th style='min-width: 250px;'>{% trans "Stock Item" %}</th>
<th>{% trans "Quantity" %}</th>
<th></th>
</tr>
</thead>
<tbody>
${table_entries}
</tbody>
</table>
`;
constructForm(`/api/build/${build_id}/allocate/`, {
method: 'POST',
fields: {},
preFormContent: html,
confirm: true,
confirmMessage: '{% trans "Confirm stock allocation" %}',
title: '{% trans "Allocate Stock Items to Build Order" %}',
afterRender: function(fields, options) {
var take_from_field = {
name: 'take_from',
model: 'stocklocation',
api_url: '{% url "api-location-list" %}',
required: false,
type: 'related field',
value: source_location,
noResults: function(query) {
return '{% trans "No matching stock locations" %}';
},
};
// Initialize "take from" field
initializeRelatedField(
take_from_field,
null,
options,
);
// Add callback to "clear" button for take_from field
addClearCallback(
'take_from',
take_from_field,
options,
);
// Initialize stock item fields
bom_items.forEach(function(bom_item) {
initializeRelatedField(
{
name: `items_stock_item_${bom_item.pk}`,
api_url: '{% url "api-stock-list" %}',
filters: {
bom_item: bom_item.pk,
in_stock: true,
available: true,
part_detail: true,
location_detail: true,
},
model: 'stockitem',
required: true,
render_part_detail: true,
render_location_detail: true,
auto_fill: true,
onSelect: function(data, field, opts) {
// Adjust the 'quantity' field based on availability
if (!('quantity' in data)) {
return;
}
// Quantity remaining to be allocated
var remaining = Math.max((bom_item.required || 0) - (bom_item.allocated || 0), 0);
// Calculate the available quantity
var available = Math.max((data.quantity || 0) - (data.allocated || 0), 0);
// Maximum amount that we need
var desired = Math.min(available, remaining);
updateFieldValue(`items_quantity_${bom_item.pk}`, desired, {}, opts);
},
adjustFilters: function(filters) {
// Restrict query to the selected location
var location = getFormFieldValue(
'take_from',
{},
{
modal: options.modal,
}
);
filters.location = location;
filters.cascade = true;
return filters;
},
noResults: function(query) {
return '{% trans "No matching stock items" %}';
}
},
null,
options,
);
});
// Add remove-row button callbacks
$(options.modal).find('.button-row-remove').click(function() {
var pk = $(this).attr('pk');
$(options.modal).find(`#allocation_row_${pk}`).remove();
});
},
onSubmit: function(fields, opts) {
// Extract elements from the form
var data = {
items: []
};
var item_pk_values = [];
bom_items.forEach(function(item) {
var quantity = getFormFieldValue(
`items_quantity_${item.pk}`,
{},
{
modal: opts.modal,
},
);
var stock_item = getFormFieldValue(
`items_stock_item_${item.pk}`,
{},
{
modal: opts.modal,
}
);
if (quantity != null) {
data.items.push({
bom_item: item.pk,
stock_item: stock_item,
quantity: quantity,
output: output_id,
});
item_pk_values.push(item.pk);
}
});
// Provide nested values
opts.nested = {
'items': item_pk_values
};
inventreePut(
opts.url,
data,
{
method: 'POST',
success: function(response) {
// Hide the modal
$(opts.modal).modal('hide');
if (options.success) {
options.success(response);
}
},
error: function(xhr) {
switch (xhr.status) {
case 400:
handleFormErrors(xhr.responseJSON, fields, opts);
break;
default:
$(opts.modal).modal('hide');
showApiError(xhr, opts.url);
break;
}
}
}
);
},
});
}
/*
* Display a table of Build orders
*/
function loadBuildTable(table, options) {
var params = options.params || {};
var filters = {};
params['part_detail'] = true;
if (!options.disableFilters) {
filters = loadTableFilters('build');
}
for (var key in params) {
filters[key] = params[key];
}
options.url = options.url || '{% url "api-build-list" %}';
var filterTarget = options.filterTarget || null;
setupFilterList('build', table, filterTarget);
$(table).inventreeTable({
method: 'get',
formatNoMatches: function() {
return '{% trans "No builds matching query" %}';
},
url: options.url,
queryParams: filters,
groupBy: false,
sidePagination: 'server',
name: 'builds',
original: params,
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
checkbox: true,
title: '{% trans "Select" %}',
searchable: false,
switchable: false,
},
{
field: 'reference',
title: '{% trans "Build" %}',
sortable: true,
switchable: true,
formatter: function(value, row) {
var prefix = global_settings.BUILDORDER_REFERENCE_PREFIX;
if (prefix) {
value = `${prefix}${value}`;
}
var html = renderLink(value, '/build/' + row.pk + '/');
if (row.overdue) {
html += makeIconBadge('fa-calendar-times icon-red', '{% trans "Build order is overdue" %}');
}
return html;
}
},
{
field: 'title',
title: '{% trans "Description" %}',
switchable: true,
},
{
field: 'part',
title: '{% trans "Part" %}',
sortable: true,
sortName: 'part__name',
formatter: function(value, row) {
var html = imageHoverIcon(row.part_detail.thumbnail);
html += renderLink(row.part_detail.full_name, `/part/${row.part}/`);
html += makePartIcons(row.part_detail);
return html;
}
},
{
field: 'quantity',
title: '{% trans "Completed" %}',
sortable: true,
formatter: function(value, row) {
return makeProgressBar(
row.completed,
row.quantity,
{
// style: 'max',
}
);
}
},
{
field: 'status',
title: '{% trans "Status" %}',
sortable: true,
formatter: function(value) {
return buildStatusDisplay(value);
},
},
{
field: 'creation_date',
title: '{% trans "Created" %}',
sortable: true,
},
{
field: 'issued_by',
title: '{% trans "Issued by" %}',
sortable: true,
formatter: function(value, row) {
if (value) {
return row.issued_by_detail.username;
} else {
return `<i>{% trans "No user information" %}</i>`;
}
}
},
{
field: 'responsible',
title: '{% trans "Responsible" %}',
sortable: true,
formatter: function(value, row) {
if (value) {
return row.responsible_detail.name;
} else {
return '{% trans "No information" %}';
}
}
},
{
field: 'target_date',
title: '{% trans "Target Date" %}',
sortable: true,
},
{
field: 'completion_date',
title: '{% trans "Completion Date" %}',
sortable: true,
},
],
});
linkButtonsToSelection(
table,
[
'#build-print-options',
]
);
}
function updateAllocationTotal(id, count, required) {
count = parseFloat(count);
$('#allocation-total-'+id).html(count);
var el = $('#allocation-panel-' + id);
el.removeClass('part-allocation-pass part-allocation-underallocated part-allocation-overallocated');
if (count < required) {
el.addClass('part-allocation-underallocated');
} else if (count > required) {
el.addClass('part-allocation-overallocated');
} else {
el.addClass('part-allocation-pass');
}
}
function loadAllocationTable(table, part_id, part, url, required, button) {
// Load the allocation table
table.bootstrapTable({
url: url,
sortable: false,
formatNoMatches: function() {
return '{% trans "No parts allocated for" %} ' + part;
},
columns: [
{
field: 'stock_item_detail',
title: '{% trans "Stock Item" %}',
formatter: function(value) {
return '' + parseFloat(value.quantity) + ' x ' + value.part_name + ' @ ' + value.location_name;
}
},
{
field: 'stock_item_detail.quantity',
title: '{% trans "Available" %}',
formatter: function(value) {
return parseFloat(value);
}
},
{
field: 'quantity',
title: '{% trans "Allocated" %}',
formatter: function(value, row) {
var html = parseFloat(value);
var bEdit = `<button class='btn item-edit-button btn-sm' type='button' title='{% trans "Edit stock allocation" %}' url='/build/item/${row.pk}/edit/'><span class='fas fa-edit'></span></button>`;
var bDel = `<button class='btn item-del-button btn-sm' type='button' title='{% trans "Delete stock allocation" %}' url='/build/item/${row.pk}/delete/'><span class='fas fa-trash-alt icon-red'></span></button>`;
html += `
<div class='btn-group' style='float: right;'>
${bEdit}
${bDel}
</div>
`;
return html;
}
}
],
});
// Callback for 'new-item' button
button.click(function() {
launchModalForm(button.attr('url'), {
success: function() {
table.bootstrapTable('refresh');
},
});
});
table.on('load-success.bs.table', function() {
// Extract table data
var results = table.bootstrapTable('getData');
var count = 0;
for (var i = 0; i < results.length; i++) {
count += parseFloat(results[i].quantity);
}
updateAllocationTotal(part_id, count, required);
});
// Button callbacks for editing and deleting the allocations
table.on('click', '.item-edit-button', function() {
var button = $(this);
launchModalForm(button.attr('url'), {
success: function() {
table.bootstrapTable('refresh');
}
});
});
table.on('click', '.item-del-button', function() {
var button = $(this);
launchModalForm(button.attr('url'), {
success: function() {
table.bootstrapTable('refresh');
}
});
});
}