mirror of
https://github.com/inventree/InvenTree.git
synced 2025-05-10 17:28:50 +00:00
(cherry picked from commit c9c93bce39e8c58921c3212b7577a9357aa3236b) Co-authored-by: Oliver <oliver.henry.walters@gmail.com>
2739 lines
85 KiB
JavaScript
2739 lines
85 KiB
JavaScript
{% load i18n %}
|
|
{% load inventree_extras %}
|
|
|
|
/* globals
|
|
addClearCallback,
|
|
buildStatusDisplay,
|
|
clearEvents,
|
|
constructExpandCollapseButtons,
|
|
constructField,
|
|
constructForm,
|
|
constructOrderTableButtons,
|
|
endDate,
|
|
formatDecimal,
|
|
FullCalendar,
|
|
getFormFieldValue,
|
|
getTableData,
|
|
global_settings,
|
|
handleFormErrors,
|
|
handleFormSuccess,
|
|
imageHoverIcon,
|
|
initializeRelatedField,
|
|
inventreeGet,
|
|
inventreeLoad,
|
|
inventreePut,
|
|
launchModalForm,
|
|
loadTableFilters,
|
|
locationDetail,
|
|
makeDeleteButton,
|
|
makeEditButton,
|
|
makeRemoveButton,
|
|
makeIconBadge,
|
|
makeIconButton,
|
|
makePartIcons,
|
|
makeProgressBar,
|
|
orderParts,
|
|
reloadBootstrapTable,
|
|
renderDate,
|
|
renderLink,
|
|
setupFilterList,
|
|
shortenString,
|
|
showAlertDialog,
|
|
showApiError,
|
|
startDate,
|
|
stockStatusDisplay,
|
|
showApiErrors,
|
|
thumbnailImage,
|
|
updateFieldValue,
|
|
wrapButtons,
|
|
yesNoLabel,
|
|
*/
|
|
|
|
/* exported
|
|
allocateStockToBuild,
|
|
autoAllocateStockToBuild,
|
|
cancelBuildOrder,
|
|
completeBuildOrder,
|
|
createBuildOutput,
|
|
duplicateBuildOrder,
|
|
editBuildOrder,
|
|
loadBuildLineTable,
|
|
loadBuildOrderAllocationTable,
|
|
loadBuildOutputTable,
|
|
loadBuildTable,
|
|
*/
|
|
|
|
|
|
function buildFormFields() {
|
|
let fields = {
|
|
reference: {
|
|
icon: 'fa-hashtag',
|
|
},
|
|
part: {
|
|
filters: {
|
|
assembly: true,
|
|
virtual: false,
|
|
}
|
|
},
|
|
title: {},
|
|
quantity: {},
|
|
project_code: {
|
|
icon: 'fa-list',
|
|
},
|
|
priority: {},
|
|
parent: {
|
|
filters: {
|
|
part_detail: true,
|
|
}
|
|
},
|
|
sales_order: {
|
|
icon: 'fa-truck',
|
|
},
|
|
batch: {},
|
|
target_date: {
|
|
icon: 'fa-calendar-alt',
|
|
},
|
|
take_from: {
|
|
icon: 'fa-sitemap',
|
|
filters: {
|
|
structural: false,
|
|
}
|
|
},
|
|
destination: {
|
|
icon: 'fa-sitemap',
|
|
filters: {
|
|
structural: false,
|
|
}
|
|
},
|
|
link: {
|
|
icon: 'fa-link',
|
|
},
|
|
issued_by: {
|
|
icon: 'fa-user',
|
|
},
|
|
responsible: {
|
|
icon: 'fa-users',
|
|
filters: {
|
|
is_active: true,
|
|
}
|
|
},
|
|
};
|
|
|
|
if (!global_settings.PROJECT_CODES_ENABLED) {
|
|
delete fields.project_code;
|
|
}
|
|
|
|
return fields;
|
|
}
|
|
|
|
/*
|
|
* Edit an existing BuildOrder via the API
|
|
*/
|
|
function editBuildOrder(pk) {
|
|
|
|
var fields = buildFormFields();
|
|
|
|
// Cannot edit "part" field after creation
|
|
delete fields['part'];
|
|
|
|
constructForm(`{% url "api-build-list" %}${pk}/`, {
|
|
fields: fields,
|
|
reload: true,
|
|
title: '{% jstrans "Edit Build Order" %}',
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Create a new build order via an API form
|
|
*/
|
|
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;
|
|
}
|
|
|
|
if (options.data) {
|
|
delete options.data.pk;
|
|
}
|
|
|
|
constructForm(`/api/build/`, {
|
|
fields: fields,
|
|
data: options.data,
|
|
follow: true,
|
|
method: 'POST',
|
|
title: '{% jstrans "Create Build Order" %}',
|
|
onSuccess: options.onSuccess,
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Duplicate an existing build order.
|
|
*/
|
|
function duplicateBuildOrder(build_id, options={}) {
|
|
|
|
inventreeGet(`{% url "api-build-list" %}${build_id}/`, {}, {
|
|
success: function(data) {
|
|
// Clear out data we do not want to be duplicated
|
|
delete data['pk'];
|
|
delete data['issued_by'];
|
|
delete data['reference'];
|
|
|
|
options.data = data;
|
|
newBuildOrder(options);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/* Construct a form to cancel a build order */
|
|
function cancelBuildOrder(build_id, options={}) {
|
|
|
|
constructForm(
|
|
`{% url "api-build-list" %}${build_id}/cancel/`,
|
|
{
|
|
method: 'POST',
|
|
title: '{% jstrans "Cancel Build Order" %}',
|
|
confirm: true,
|
|
fields: {
|
|
remove_allocated_stock: {},
|
|
remove_incomplete_outputs: {},
|
|
},
|
|
preFormContent: function(opts) {
|
|
var html = `
|
|
<div class='alert alert-block alert-info'>
|
|
{% jstrans "Are you sure you wish to cancel this build?" %}
|
|
</div>`;
|
|
|
|
if (opts.context.has_allocated_stock) {
|
|
html += `
|
|
<div class='alert alert-block alert-warning'>
|
|
{% jstrans "Stock items have been allocated to this build order" %}
|
|
</div>`;
|
|
}
|
|
|
|
if (opts.context.incomplete_outputs) {
|
|
html += `
|
|
<div class='alert alert-block alert-warning'>
|
|
{% jstrans "There are incomplete outputs remaining for this build order" %}
|
|
</div>`;
|
|
}
|
|
|
|
return html;
|
|
},
|
|
onSuccess: function(response) {
|
|
handleFormSuccess(response, options);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
|
|
/* Construct a form to "complete" (finish) a build order */
|
|
function completeBuildOrder(build_id, options={}) {
|
|
|
|
constructForm(`{% url "api-build-list" %}${build_id}/finish/`, {
|
|
fieldsFunction: function(opts) {
|
|
var ctx = opts.context || {};
|
|
|
|
var fields = {
|
|
accept_unallocated: {},
|
|
accept_overallocated: {},
|
|
accept_incomplete: {},
|
|
};
|
|
|
|
// Hide "accept overallocated" field if the build is *not* overallocated
|
|
if (!ctx.overallocated) {
|
|
delete fields.accept_overallocated;
|
|
}
|
|
|
|
// Hide "accept incomplete" field if the build has been completed
|
|
if (!ctx.remaining || ctx.remaining == 0) {
|
|
delete fields.accept_incomplete;
|
|
}
|
|
|
|
// Hide "accept unallocated" field if the build is fully allocated
|
|
if (ctx.allocated) {
|
|
delete fields.accept_unallocated;
|
|
}
|
|
|
|
return fields;
|
|
},
|
|
preFormContent: function(opts) {
|
|
var ctx = opts.context || {};
|
|
|
|
var html = '';
|
|
|
|
if (ctx.allocated && ctx.remaining == 0 && ctx.incomplete == 0) {
|
|
html += `
|
|
<div class='alert alert-block alert-success'>
|
|
{% jstrans "Build order is ready to be completed" %}'
|
|
</div>`;
|
|
} else {
|
|
|
|
if (ctx.incomplete > 0) {
|
|
html += `
|
|
<div class='alert alert-block alert-danger'>
|
|
<strong>{% jstrans "Build order has incomplete outputs" %}</strong><br>
|
|
{% jstrans "This build order cannot be completed as there are incomplete outputs" %}
|
|
</div>`;
|
|
} else {
|
|
html += `
|
|
<div class='alert alert-block alert-danger'>
|
|
<strong>{% jstrans "Build Order is incomplete" %}</strong>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (!ctx.allocated) {
|
|
html += `<div class='alert alert-block alert-warning'>{% jstrans "Required stock has not been fully allocated" %}</div>`;
|
|
}
|
|
|
|
if (ctx.remaining > 0) {
|
|
html += `<div class='alert alert-block alert-warning'>{% jstrans "Required build quantity has not been completed" %}</div>`;
|
|
}
|
|
}
|
|
|
|
return html;
|
|
},
|
|
reload: true,
|
|
confirm: true,
|
|
title: '{% jstrans "Complete Build Order" %}',
|
|
method: 'POST',
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Construct a new build output against the provided build
|
|
*/
|
|
function createBuildOutput(build_id, options) {
|
|
|
|
// Request build order information from the server
|
|
inventreeGet(
|
|
`{% url "api-build-list" %}${build_id}/`,
|
|
{},
|
|
{
|
|
success: function(build) {
|
|
|
|
var html = '';
|
|
|
|
var trackable = build.part_detail.trackable;
|
|
var remaining = Math.max(0, build.quantity - build.completed);
|
|
|
|
var fields = {
|
|
quantity: {
|
|
value: remaining,
|
|
},
|
|
serial_numbers: {
|
|
hidden: !trackable,
|
|
required: options.trackable_parts || trackable,
|
|
},
|
|
batch_code: {},
|
|
auto_allocate: {
|
|
hidden: !trackable,
|
|
},
|
|
};
|
|
|
|
// Work out the next available serial numbers
|
|
inventreeGet(`{% url "api-part-list" %}${build.part}/serial-numbers/`, {}, {
|
|
success: function(data) {
|
|
if (data.next) {
|
|
fields.serial_numbers.placeholder = `{% jstrans "Next available serial number" %}: ${data.next}`;
|
|
} else if (data.latest) {
|
|
fields.serial_numbers.placeholder = `{% jstrans "Latest serial number" %}: ${data.latest}`;
|
|
}
|
|
},
|
|
async: false,
|
|
});
|
|
|
|
if (options.trackable_parts) {
|
|
html += `
|
|
<div class='alert alert-block alert-info'>
|
|
{% jstrans "The Bill of Materials contains trackable parts" %}.<br>
|
|
{% jstrans "Build outputs must be generated individually" %}.
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
if (trackable) {
|
|
html += `
|
|
<div class='alert alert-block alert-info'>
|
|
{% jstrans "Trackable parts can have serial numbers specified" %}<br>
|
|
{% jstrans "Enter serial numbers to generate multiple single build outputs" %}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
constructForm(`{% url "api-build-list" %}${build_id}/create-output/`, {
|
|
method: 'POST',
|
|
title: '{% jstrans "Create Build Output" %}',
|
|
confirm: true,
|
|
fields: fields,
|
|
preFormContent: html,
|
|
onSuccess: function(response) {
|
|
reloadBootstrapTable(options.table || '#build-output-table');
|
|
},
|
|
});
|
|
|
|
}
|
|
}
|
|
);
|
|
|
|
}
|
|
|
|
|
|
/*
|
|
* Construct a set of output buttons for a particular build output
|
|
*/
|
|
function makeBuildOutputButtons(output_id, build_info, options={}) {
|
|
|
|
var html = '';
|
|
|
|
// Tracked parts? Must be individually allocated
|
|
if (options.has_tracked_lines) {
|
|
|
|
// Add a button to allocate stock against this build output
|
|
html += makeIconButton(
|
|
'fa-sign-in-alt icon-blue',
|
|
'button-output-allocate',
|
|
output_id,
|
|
'{% jstrans "Allocate stock items to this build output" %}',
|
|
);
|
|
|
|
// Add a button to deallocate stock from this build output
|
|
html += makeIconButton(
|
|
'fa-minus-circle icon-red',
|
|
'button-output-deallocate',
|
|
output_id,
|
|
'{% jstrans "Deallocate stock from build output" %}',
|
|
);
|
|
}
|
|
|
|
// Add a button to "complete" this build output
|
|
html += makeIconButton(
|
|
'fa-check-circle icon-green',
|
|
'button-output-complete',
|
|
output_id,
|
|
'{% jstrans "Complete build output" %}',
|
|
);
|
|
|
|
// Add a button to "scrap" the build output
|
|
html += makeIconButton(
|
|
'fa-times-circle icon-red',
|
|
'button-output-scrap',
|
|
output_id,
|
|
'{% jstrans "Scrap build output" %}',
|
|
);
|
|
|
|
// Add a button to "remove" this build output
|
|
html += makeDeleteButton(
|
|
'button-output-remove',
|
|
output_id,
|
|
'{% jstrans "Delete build output" %}',
|
|
);
|
|
|
|
return wrapButtons(html);
|
|
}
|
|
|
|
|
|
/*
|
|
* Deallocate 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 deallocateStock(build_id, options={}) {
|
|
|
|
var url = `{% url "api-build-list" %}${build_id}/unallocate/`;
|
|
|
|
var html = `
|
|
<div class='alert alert-block alert-warning'>
|
|
{% jstrans "Are you sure you wish to deallocate the selected stock items from this build?" %}
|
|
</dvi>
|
|
`;
|
|
|
|
constructForm(url, {
|
|
method: 'POST',
|
|
confirm: true,
|
|
preFormContent: html,
|
|
fields: {
|
|
output: {
|
|
hidden: true,
|
|
value: options.output,
|
|
},
|
|
build_line: {
|
|
hidden: true,
|
|
value: options.build_line,
|
|
},
|
|
},
|
|
title: '{% jstrans "Deallocate Stock Items" %}',
|
|
onSuccess: function(response, opts) {
|
|
if (options.onSuccess) {
|
|
options.onSuccess(response, opts);
|
|
} else if (options.table) {
|
|
// Reload the parent table
|
|
$(options.table).bootstrapTable('refresh');
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Helper function to render a single build output in a modal form
|
|
*/
|
|
function renderBuildOutput(output, options={}) {
|
|
let pk = output.pk;
|
|
|
|
let output_html = imageHoverIcon(output.part_detail.thumbnail);
|
|
|
|
if (output.quantity == 1 && output.serial) {
|
|
output_html += `{% jstrans "Serial Number" %}: ${output.serial}`;
|
|
} else {
|
|
output_html += `{% jstrans "Quantity" %}: ${output.quantity}`;
|
|
if (output.part_detail && output.part_detail.units) {
|
|
output_html += ` ${output.part_detail.units} `;
|
|
}
|
|
}
|
|
|
|
let buttons = `<div class='btn-group float-right' role='group'>`;
|
|
|
|
buttons += makeRemoveButton('button-row-remove', pk, '{% jstrans "Remove row" %}');
|
|
|
|
buttons += '</div>';
|
|
|
|
let field = constructField(
|
|
`outputs_output_${pk}`,
|
|
{
|
|
type: 'raw',
|
|
html: output_html,
|
|
},
|
|
{
|
|
hideLabels: true,
|
|
}
|
|
);
|
|
|
|
let quantity_field = '';
|
|
|
|
if (options.adjust_quantity) {
|
|
quantity_field = constructField(
|
|
`outputs_quantity_${pk}`,
|
|
{
|
|
type: 'decimal',
|
|
value: output.quantity,
|
|
min_value: 0,
|
|
max_value: output.quantity,
|
|
required: true,
|
|
},
|
|
{
|
|
hideLabels: true,
|
|
}
|
|
);
|
|
|
|
quantity_field = `<td>${quantity_field}</td>`;
|
|
}
|
|
|
|
let html = `
|
|
<tr id='output_row_${pk}'>
|
|
<td>${field}</td>
|
|
<td>${output.part_detail.full_name}</td>
|
|
${quantity_field}
|
|
<td>${buttons}</td>
|
|
</tr>`;
|
|
|
|
return html;
|
|
}
|
|
|
|
|
|
/**
|
|
* Launch a modal form to complete selected build outputs
|
|
*/
|
|
function completeBuildOutputs(build_id, outputs, options={}) {
|
|
|
|
if (outputs.length == 0) {
|
|
showAlertDialog(
|
|
'{% jstrans "Select Build Outputs" %}',
|
|
'{% jstrans "At least one build output must be selected" %}',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Construct table entries
|
|
var table_entries = '';
|
|
|
|
outputs.forEach(function(output) {
|
|
table_entries += renderBuildOutput(output);
|
|
});
|
|
|
|
var html = `
|
|
<div class='alert alert-block alert-success'>
|
|
{% jstrans "Selected build outputs will be marked as complete" %}
|
|
</div>
|
|
<table class='table table-striped table-condensed' id='build-complete-table'>
|
|
<thead>
|
|
<th colspan='2'>{% jstrans "Output" %}</th>
|
|
<th><!-- Actions --></th>
|
|
</thead>
|
|
<tbody>
|
|
${table_entries}
|
|
</tbody>
|
|
</table>`;
|
|
|
|
constructForm(`{% url "api-build-list" %}${build_id}/complete/`, {
|
|
method: 'POST',
|
|
preFormContent: html,
|
|
fields: {
|
|
status: {},
|
|
location: {
|
|
filters: {
|
|
structural: false,
|
|
},
|
|
tree_picker: {
|
|
url: '{% url "api-location-tree" %}',
|
|
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
|
|
},
|
|
},
|
|
notes: {
|
|
icon: 'fa-sticky-note',
|
|
},
|
|
accept_incomplete_allocation: {},
|
|
},
|
|
confirm: true,
|
|
title: '{% jstrans "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),
|
|
notes: getFormFieldValue('notes', {}, opts),
|
|
accept_incomplete_allocation: getFormFieldValue('accept_incomplete_allocation', {type: 'boolean'}, 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;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Launch a modal form to scrap selected build outputs.
|
|
* Scrapped outputs are marked as "complete", but with the "rejected" code
|
|
* These outputs are not included in build completion calculations.
|
|
*/
|
|
function scrapBuildOutputs(build_id, outputs, options={}) {
|
|
|
|
if (outputs.length == 0) {
|
|
showAlertDialog(
|
|
'{% jstrans "Select Build Outputs" %}',
|
|
'{% jstrans "At least one build output must be selected" %}',
|
|
);
|
|
return;
|
|
}
|
|
|
|
let table_entries = '';
|
|
|
|
outputs.forEach(function(output) {
|
|
table_entries += renderBuildOutput(output, {
|
|
adjust_quantity: true,
|
|
});
|
|
});
|
|
|
|
var html = `
|
|
<div class='alert alert-block alert-danger'>
|
|
{% jstrans "Selected build outputs will be marked as scrapped" %}
|
|
<ul>
|
|
<li>{% jstrans "Scrapped output are marked as rejected" %}</li>
|
|
<li>{% jstrans "Allocated stock items will no longer be available" %}</li>
|
|
<li>{% jstrans "The completion status of the build order will not be adjusted" %}</li>
|
|
</ul>
|
|
</div>
|
|
<table class='table table-striped table-condensed' id='build-scrap-table'>
|
|
<thead>
|
|
<th colspan='2'>{% jstrans "Output" %}</th>
|
|
<th>{% jstrans "Quantity" %}</th>
|
|
<th><!-- Actions --></th>
|
|
</thead>
|
|
<tbody>
|
|
${table_entries}
|
|
</tbody>
|
|
</table>`;
|
|
|
|
constructForm(`{% url "api-build-list" %}${build_id}/scrap-outputs/`, {
|
|
method: 'POST',
|
|
preFormContent: html,
|
|
fields: {
|
|
location: {
|
|
filters: {
|
|
structural: false,
|
|
},
|
|
tree_picker: {
|
|
url: '{% url "api-location-tree" %}',
|
|
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
|
|
},
|
|
},
|
|
notes: {},
|
|
discard_allocations: {},
|
|
},
|
|
confirm: true,
|
|
title: '{% jstrans "Scrap Build Outputs" %}',
|
|
afterRender: function(fields, opts) {
|
|
// Setup callbacks to remove outputs
|
|
$(opts.modal).find('.button-row-remove').click(function() {
|
|
let pk = $(this).attr('pk');
|
|
$(opts.modal).find(`#output_row_${pk}`).remove();
|
|
});
|
|
},
|
|
onSubmit: function(fields, opts) {
|
|
let data = {
|
|
outputs: [],
|
|
location: getFormFieldValue('location', {}, opts),
|
|
notes: getFormFieldValue('notes', {}, opts),
|
|
discard_allocations: getFormFieldValue('discard_allocations', {type: 'boolean'}, opts),
|
|
};
|
|
|
|
let output_pk_values = [];
|
|
|
|
outputs.forEach(function(output) {
|
|
let pk = output.pk;
|
|
let row = $(opts.modal).find(`#output_row_${pk}`);
|
|
let quantity = getFormFieldValue(`outputs_quantity_${pk}`, {}, opts);
|
|
|
|
if (row.exists()) {
|
|
data.outputs.push({
|
|
output: pk,
|
|
quantity: quantity,
|
|
});
|
|
|
|
output_pk_values.push(pk);
|
|
}
|
|
});
|
|
|
|
opts.nested = {
|
|
'outputs': output_pk_values,
|
|
};
|
|
|
|
inventreePut(
|
|
opts.url,
|
|
data,
|
|
{
|
|
method: 'POST',
|
|
success: function(response) {
|
|
$(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;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Launch a modal form to delete selected build outputs.
|
|
* Deleted outputs are expunged from the database.
|
|
*/
|
|
function deleteBuildOutputs(build_id, outputs, options={}) {
|
|
|
|
if (outputs.length == 0) {
|
|
showAlertDialog(
|
|
'{% jstrans "Select Build Outputs" %}',
|
|
'{% jstrans "At least one build output must be selected" %}',
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Construct table entries
|
|
var table_entries = '';
|
|
|
|
outputs.forEach(function(output) {
|
|
table_entries += renderBuildOutput(output);
|
|
});
|
|
|
|
var html = `
|
|
<div class='alert alert-block alert-danger'>
|
|
{% jstrans "Selected build outputs will be deleted" %}
|
|
<ul>
|
|
<li>{% jstrans "Build output data will be permanently deleted" %}</li>
|
|
<li>{% jstrans "Allocated stock items will be returned to stock" %}</li>
|
|
</ul>
|
|
</div>
|
|
<table class='table table-striped table-condensed' id='build-complete-table'>
|
|
<thead>
|
|
<th colspan='2'>{% jstrans "Output" %}</th>
|
|
<th><!-- Actions --></th>
|
|
</thead>
|
|
<tbody>
|
|
${table_entries}
|
|
</tbody>
|
|
</table>`;
|
|
|
|
constructForm(`{% url "api-build-list" %}${build_id}/delete-outputs/`, {
|
|
method: 'POST',
|
|
preFormContent: html,
|
|
fields: {},
|
|
confirm: true,
|
|
title: '{% jstrans "Delete 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) {
|
|
var data = {
|
|
outputs: [],
|
|
};
|
|
|
|
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);
|
|
}
|
|
});
|
|
|
|
opts.nested = {
|
|
'outputs': output_pk_values,
|
|
};
|
|
|
|
inventreePut(
|
|
opts.url,
|
|
data,
|
|
{
|
|
method: 'POST',
|
|
success: function(response) {
|
|
$(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;
|
|
options.params['stock_detail'] = true;
|
|
|
|
var filters = loadTableFilters('buildorderallocation', options.params);
|
|
|
|
setupFilterList('buildorderallocation', $(table));
|
|
|
|
$(table).inventreeTable({
|
|
url: '{% url "api-build-item-list" %}',
|
|
queryParams: filters,
|
|
name: 'buildorderallocation',
|
|
groupBy: false,
|
|
search: false,
|
|
sortable: true,
|
|
paginationVAlign: 'bottom',
|
|
original: options.params,
|
|
formatNoMatches: function() {
|
|
return '{% jstrans "No build order allocations found" %}';
|
|
},
|
|
columns: [
|
|
{
|
|
field: 'pk',
|
|
visible: false,
|
|
switchable: false,
|
|
},
|
|
{
|
|
field: 'build',
|
|
sortable: true,
|
|
switchable: false,
|
|
title: '{% jstrans "Build Order" %}',
|
|
formatter: function(value, row) {
|
|
let ref = `${row.build_detail.reference}`;
|
|
let html = renderLink(ref, `/build/${row.build}/`);
|
|
|
|
html += `- <small>${row.build_detail.title}</small>`;
|
|
|
|
html += buildStatusDisplay(row.build_detail.status, {
|
|
classes: 'float-right',
|
|
});
|
|
|
|
return html;
|
|
}
|
|
},
|
|
{
|
|
field: 'quantity',
|
|
sortable: true,
|
|
title: '{% jstrans "Allocated Quantity" %}',
|
|
formatter: function(value, row) {
|
|
let link = `/stock/item/${row.stock_item}/`;
|
|
let text = formatDecimal(value);
|
|
|
|
return renderLink(text, link);
|
|
}
|
|
},
|
|
{
|
|
field: 'location_detail',
|
|
title: '{% jstrans "Location" %}',
|
|
formatter: function(value, row) {
|
|
|
|
if (!value) {
|
|
return '{% jstrans "Location not specified" %}';
|
|
}
|
|
|
|
let item = row.stock_item_detail;
|
|
item.location_detail = row.location_detail;
|
|
|
|
return locationDetail(item, true);
|
|
}
|
|
},
|
|
]
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Construct a set of actions for the build output table
|
|
*/
|
|
function makeBuildOutputActions(build_info) {
|
|
|
|
return [
|
|
{
|
|
label: 'complete',
|
|
title: '{% jstrans "Complete outputs" %}',
|
|
icon: 'fa-check-circle icon-green',
|
|
permission: 'build.add',
|
|
callback: function(data) {
|
|
completeBuildOutputs(
|
|
build_info.pk,
|
|
data,
|
|
{
|
|
success: function() {
|
|
$('#build-output-table').bootstrapTable('refresh'); // Reload the "in progress" table
|
|
$('#build-stock-table').bootstrapTable('refresh'); // Reload the "completed" table
|
|
}
|
|
}
|
|
);
|
|
},
|
|
},
|
|
{
|
|
label: 'scrap',
|
|
title: '{% jstrans "Scrap outputs" %}',
|
|
icon: 'fa-times-circle icon-red',
|
|
permission: 'build.change',
|
|
callback: function(data) {
|
|
scrapBuildOutputs(
|
|
build_info.pk,
|
|
data,
|
|
{
|
|
success: function() {
|
|
$('#build-output-table').bootstrapTable('refresh'); // Reload the "in progress" table
|
|
$('#build-stock-table').bootstrapTable('refresh'); // Reload the "completed" table
|
|
}
|
|
}
|
|
);
|
|
},
|
|
},
|
|
{
|
|
label: 'delete',
|
|
title: '{% jstrans "Delete outputs" %}',
|
|
icon: 'fa-trash-alt icon-red',
|
|
permission: 'build.delete',
|
|
callback: function(data) {
|
|
deleteBuildOutputs(
|
|
build_info.pk,
|
|
data,
|
|
{
|
|
success: function() {
|
|
$('#build-output-table').bootstrapTable('refresh'); // Reload the "in progress" table
|
|
$('#build-stock-table').bootstrapTable('refresh'); // Reload the "completed" table
|
|
}
|
|
}
|
|
)
|
|
},
|
|
}
|
|
];
|
|
}
|
|
|
|
|
|
/*
|
|
* Display a "build output" table for a particular build.
|
|
*
|
|
* This displays a list of "active" (i.e. "in production") build outputs (stock items) for a given build.
|
|
*
|
|
* - Any required tests are displayed here for each output
|
|
* - Additionally, if any tracked items are present in the build, the allocated items are displayed
|
|
*
|
|
*/
|
|
function loadBuildOutputTable(build_info, options={}) {
|
|
|
|
var table = options.table || '#build-output-table';
|
|
|
|
var params = options.params || {};
|
|
|
|
// test templates for the part being assembled
|
|
let test_templates = [];
|
|
|
|
// tracked line items for this build
|
|
let has_tracked_lines = false;
|
|
|
|
// Mandatory query filters
|
|
params.part_detail = true;
|
|
params.tests = true;
|
|
params.is_building = true;
|
|
params.build = build_info.pk;
|
|
|
|
var filters = Object.assign({}, params);
|
|
|
|
setupFilterList('builditems', $(table), options.filterTarget || '#filter-list-incompletebuilditems', {
|
|
labels: {
|
|
url: '{% url "api-stockitem-label-list" %}',
|
|
key: 'item',
|
|
},
|
|
singular_name: '{% jstrans "build output" %}',
|
|
plural_name: '{% jstrans "build outputs" %}',
|
|
custom_actions: [{
|
|
label: 'buildoutput',
|
|
icon: 'fa-tools',
|
|
title: '{% jstrans "Build output actions" %}',
|
|
actions: makeBuildOutputActions(build_info),
|
|
}]
|
|
});
|
|
|
|
// Request list of required tests for the part being assembled
|
|
inventreeGet(
|
|
'{% url "api-part-test-template-list" %}',
|
|
{
|
|
part: build_info.part,
|
|
},
|
|
{
|
|
async: false,
|
|
success: function(response) {
|
|
test_templates = [];
|
|
response.forEach(function(item) {
|
|
// Only include "required" tests
|
|
if (item.required) {
|
|
test_templates.push(item);
|
|
}
|
|
});
|
|
},
|
|
error: function() {
|
|
test_templates = [];
|
|
}
|
|
}
|
|
);
|
|
|
|
// Callback function to load the allocated stock items
|
|
function reloadOutputAllocations() {
|
|
inventreeGet(
|
|
'{% url "api-build-line-list" %}',
|
|
{
|
|
build: build_info.pk,
|
|
tracked: true,
|
|
},
|
|
{
|
|
success: function(response) {
|
|
let build_lines = response.results || response;
|
|
let table_data = $(table).bootstrapTable('getData');
|
|
|
|
has_tracked_lines = build_lines.length > 0;
|
|
|
|
/* Iterate through each active build output and update allocations
|
|
* For each build output, we need to:
|
|
* - Append any existing allocations
|
|
* - Work out how many lines are "fully allocated"
|
|
*/
|
|
for (var ii = 0; ii < table_data.length; ii++) {
|
|
let output = table_data[ii];
|
|
|
|
let fully_allocated = 0;
|
|
|
|
// Construct a list of allocations for this output
|
|
let lines = [];
|
|
|
|
// Iterate through each tracked build line item
|
|
for (let jj = 0; jj < build_lines.length; jj++) {
|
|
|
|
// Create a local copy of the build line
|
|
let line = Object.assign({}, build_lines[jj]);
|
|
|
|
let required = line.bom_item_detail.quantity * output.quantity;
|
|
|
|
let allocations = [];
|
|
let allocated = 0;
|
|
|
|
// Iterate through each allocation for this line item
|
|
for (let kk = 0; kk < line.allocations.length; kk++) {
|
|
let allocation = line.allocations[kk];
|
|
|
|
if (allocation.install_into == output.pk) {
|
|
allocations.push(allocation);
|
|
allocated += allocation.quantity;
|
|
}
|
|
}
|
|
|
|
line.allocations = allocations;
|
|
line.allocated = allocated;
|
|
line.quantity = required;
|
|
|
|
if (allocated >= required) {
|
|
fully_allocated += 1;
|
|
}
|
|
|
|
lines.push(line);
|
|
}
|
|
|
|
// Push the row back in
|
|
output.lines = lines;
|
|
output.fully_allocated = fully_allocated;
|
|
table_data[ii] = output;
|
|
}
|
|
|
|
// Update the table data
|
|
$(table).bootstrapTable('load', table_data);
|
|
|
|
if (has_tracked_lines) {
|
|
$(table).bootstrapTable('showColumn', 'fully_allocated');
|
|
} else {
|
|
$(table).bootstrapTable('hideColumn', 'fully_allocated');
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
// Callback function to construct a child table
|
|
function constructOutputSubTable(index, row, element) {
|
|
let sub_table_id = `output-table-${row.pk}`;
|
|
|
|
element.html(`
|
|
<div class='sub-table'>
|
|
<table class='table table-striped table-condensed' id='${sub_table_id}'></table>
|
|
</div>
|
|
`);
|
|
|
|
loadBuildLineTable(
|
|
`#${sub_table_id}`,
|
|
build_info.pk,
|
|
{
|
|
output: row.pk,
|
|
data: row.lines,
|
|
}
|
|
);
|
|
}
|
|
|
|
// Return the "passed test count" for a given row
|
|
function getPassedTestCount(row) {
|
|
let passed_tests = 0;
|
|
|
|
// Iterate through the available test templates
|
|
test_templates.forEach(function(test) {
|
|
// Iterate through all the "test results" for the given stock item
|
|
// If the keys match, update the result
|
|
// As they are returned in order, the "latest" result is the one we use
|
|
|
|
let final_result = false;
|
|
|
|
row.tests.forEach(function(result) {
|
|
if (result.key == test.key) {
|
|
final_result = result.result;
|
|
}
|
|
});
|
|
|
|
if (final_result) {
|
|
passed_tests += 1;
|
|
}
|
|
});
|
|
|
|
return passed_tests;
|
|
}
|
|
|
|
// Now, construct the actual table
|
|
$(table).inventreeTable({
|
|
url: '{% url "api-stock-list" %}',
|
|
queryParams: filters,
|
|
original: params,
|
|
showColumns: true,
|
|
uniqueId: 'pk',
|
|
name: 'build-outputs',
|
|
sortable: true,
|
|
search: true,
|
|
sidePagination: 'client',
|
|
detailView: true,
|
|
detailFilter: function(index, row) {
|
|
return has_tracked_lines;
|
|
},
|
|
detailFormatter: function(index, row, element) {
|
|
return constructOutputSubTable(index, row, element);
|
|
},
|
|
formatNoMatches: function() {
|
|
return '{% jstrans "No active build outputs found" %}';
|
|
},
|
|
onLoadSuccess: function() {
|
|
reloadOutputAllocations();
|
|
},
|
|
buttons: constructExpandCollapseButtons(table),
|
|
columns: [
|
|
{
|
|
title: '',
|
|
visible: true,
|
|
checkbox: true,
|
|
switchable: false,
|
|
},
|
|
{
|
|
field: 'part',
|
|
title: '{% jstrans "Part" %}',
|
|
switchable: false,
|
|
formatter: function(value, row) {
|
|
return imageHoverIcon(row.part_detail.thumbnail) +
|
|
renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`) +
|
|
makePartIcons(row.part_detail);
|
|
}
|
|
},
|
|
{
|
|
field: 'quantity',
|
|
title: '{% jstrans "Build Output" %}',
|
|
switchable: false,
|
|
sortable: true,
|
|
sorter: function(fieldA, fieldB, rowA, rowB) {
|
|
|
|
let serialA = parseInt(rowA.serial);
|
|
let serialB = parseInt(rowB.serial);
|
|
|
|
// Fallback to string representation
|
|
if (isNaN(serialA)) {
|
|
serialA = rowA.serial;
|
|
} else if (isNaN(serialB)) {
|
|
serialB = rowB.serial;
|
|
}
|
|
|
|
if (serialA && !serialB) {
|
|
// Only rowA has a serial number
|
|
return 1;
|
|
} else if (serialB && !serialA) {
|
|
// Only rowB has a serial number
|
|
return -1;
|
|
} else if (serialA && serialB) {
|
|
// Both rows have serial numbers
|
|
if (serialA > serialB) {
|
|
return 1;
|
|
} else if (serialA < serialB) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else {
|
|
// Neither row has a serial number
|
|
if (rowA.quantity > rowB.quantity) {
|
|
return 1;
|
|
} else if (rowA.quantity < rowB.quantity) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
},
|
|
formatter: function(value, row) {
|
|
let text = '';
|
|
|
|
if (row.serial && row.quantity == 1) {
|
|
text = `{% jstrans "Serial Number" %}: ${row.serial}`;
|
|
} else {
|
|
text = `{% jstrans "Quantity" %}: ${row.quantity}`;
|
|
|
|
}
|
|
|
|
text = renderLink(text, `/stock/item/${row.pk}/`);
|
|
|
|
if (row.part_detail && row.part_detail.units) {
|
|
text += ` <small>[${row.part_detail.units}]</small>`;
|
|
}
|
|
|
|
if (row.batch) {
|
|
text += ` <small>({% jstrans "Batch" %}: ${row.batch})</small>`;
|
|
}
|
|
|
|
text += stockStatusDisplay(row.status, {classes: 'float-right'});
|
|
|
|
return text;
|
|
}
|
|
},
|
|
{
|
|
field: 'fully_allocated',
|
|
title: '{% jstrans "Allocated Lines" %}',
|
|
visible: false,
|
|
sortable: true,
|
|
switchable: false,
|
|
formatter: function(value, row) {
|
|
if (!row.lines) {
|
|
return '-';
|
|
}
|
|
|
|
return makeProgressBar(row.fully_allocated, row.lines.length);
|
|
}
|
|
},
|
|
{
|
|
field: 'tests',
|
|
title: '{% jstrans "Required Tests" %}',
|
|
visible: test_templates.length > 0,
|
|
switchable: true,
|
|
sortable: true,
|
|
sorter: function(valueA, valueB, rowA, rowB) {
|
|
let nA = getPassedTestCount(rowA);
|
|
let nB = getPassedTestCount(rowB);
|
|
|
|
if (nA > nB) {
|
|
return 1;
|
|
} else if (nA < nB) {
|
|
return -1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
formatter: function(value, row) {
|
|
if (row.tests) {
|
|
return makeProgressBar(
|
|
getPassedTestCount(row),
|
|
test_templates.length
|
|
);
|
|
}
|
|
}
|
|
},
|
|
{
|
|
field: 'actions',
|
|
title: '',
|
|
switchable: false,
|
|
formatter: function(value, row) {
|
|
return makeBuildOutputButtons(
|
|
row.pk,
|
|
build_info,
|
|
{
|
|
has_tracked_lines: has_tracked_lines,
|
|
}
|
|
)
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
/* Callbacks for the build output buttons */
|
|
|
|
// Allocate stock button
|
|
$(table).on('click', '.button-output-allocate', function() {
|
|
let pk = $(this).attr('pk');
|
|
|
|
// Retrieve build output row
|
|
let output = $(table).bootstrapTable('getRowByUniqueId', pk);
|
|
let lines = output.lines || [];
|
|
|
|
allocateStockToBuild(
|
|
build_info.pk,
|
|
lines,
|
|
{
|
|
output: pk,
|
|
success: function() {
|
|
$(table).bootstrapTable('refresh');
|
|
$('#build-stock-table').bootstrapTable('refresh');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
// Deallocate stock button
|
|
$(table).on('click', '.button-output-deallocate', function() {
|
|
let pk = $(this).attr('pk');
|
|
|
|
deallocateStock(build_info.pk, {
|
|
output: pk,
|
|
table: table
|
|
});
|
|
});
|
|
|
|
// Complete build output button
|
|
$(table).on('click', '.button-output-complete', function() {
|
|
let pk = $(this).attr('pk');
|
|
let output = $(table).bootstrapTable('getRowByUniqueId', pk);
|
|
|
|
completeBuildOutputs(
|
|
build_info.pk,
|
|
[output],
|
|
{
|
|
success: function() {
|
|
$(table).bootstrapTable('refresh');
|
|
$('#build-stock-table').bootstrapTable('refresh');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
// Scrap build output button
|
|
$(table).on('click', '.button-output-scrap', function() {
|
|
let pk = $(this).attr('pk');
|
|
let output = $(table).bootstrapTable('getRowByUniqueId', pk);
|
|
|
|
scrapBuildOutputs(
|
|
build_info.pk,
|
|
[output],
|
|
{
|
|
success: function() {
|
|
$(table).bootstrapTable('refresh');
|
|
$('#build-stock-table').bootstrapTable('refresh');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
// Remove build output button
|
|
$(table).on('click', '.button-output-remove', function() {
|
|
let pk = $(this).attr('pk');
|
|
let output = $(table).bootstrapTable('getRowByUniqueId', pk);
|
|
|
|
deleteBuildOutputs(
|
|
build_info.pk,
|
|
[output],
|
|
{
|
|
success: function() {
|
|
$(table).bootstrapTable('refresh');
|
|
$('#build-stock-table').bootstrapTable('refresh');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
// Delete multiple build outputs
|
|
$('#multi-output-delete').click(function() {
|
|
var outputs = getTableData(table);
|
|
|
|
deleteBuildOutputs(
|
|
build_info.pk,
|
|
outputs,
|
|
{
|
|
success: function() {
|
|
// Reload the "in progress" table
|
|
$('#build-output-table').bootstrapTable('refresh');
|
|
|
|
// Reload the "completed" table
|
|
$('#build-stock-table').bootstrapTable('refresh');
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
$('#outputs-expand').click(function() {
|
|
$(table).bootstrapTable('expandAllRows');
|
|
});
|
|
|
|
$('#outputs-collapse').click(function() {
|
|
$(table).bootstrapTable('collapseAllRows');
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Allocate stock items to a build
|
|
*
|
|
* arguments:
|
|
* - buildId: ID / PK value for the build
|
|
* - partId: ID / PK value for the part being built
|
|
* - line_items: A list of BuildItem 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, line_items, options={}) {
|
|
|
|
if (line_items.length == 0) {
|
|
|
|
showAlertDialog(
|
|
'{% jstrans "Select Parts" %}',
|
|
'{% jstrans "You must select at least one part to allocate" %}',
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
let build = null;
|
|
|
|
// Extract build information
|
|
inventreeGet(`{% url "api-build-list" %}${build_id}/`, {}, {
|
|
async: false,
|
|
success: function(response) {
|
|
build = response;
|
|
}
|
|
});
|
|
|
|
if (!build) {
|
|
console.error(`Failed to find build ${build_id}`);
|
|
return;
|
|
}
|
|
|
|
// ID of the associated "build output" (stock item) (or null)
|
|
var output_id = options.output || null;
|
|
|
|
var auto_fill_filters = {};
|
|
|
|
var source_location = options.source_location;
|
|
|
|
if (output_id) {
|
|
// Request information on the particular build output (stock item)
|
|
inventreeGet(`{% url "api-stock-list" %}${output_id}/`, {}, {
|
|
async: false,
|
|
success: function(output) {
|
|
if (output.quantity == 1 && output.serial != null) {
|
|
auto_fill_filters.serial = output.serial;
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
function renderBuildLineRow(build_line, quantity) {
|
|
|
|
var pk = build_line.pk;
|
|
var sub_part = build_line.part_detail;
|
|
|
|
var thumb = thumbnailImage(sub_part.thumbnail);
|
|
|
|
var delete_button = `<div class='btn-group float-right' role='group'>`;
|
|
|
|
delete_button += makeRemoveButton(
|
|
'button-row-remove',
|
|
pk,
|
|
'{% jstrans "Remove row" %}',
|
|
);
|
|
|
|
delete_button += `</div>`;
|
|
|
|
var quantity_input = constructField(
|
|
`items_quantity_${pk}`,
|
|
{
|
|
type: 'decimal',
|
|
min_value: 0,
|
|
value: quantity || 0,
|
|
title: '{% jstrans "Specify stock allocation quantity" %}',
|
|
required: true,
|
|
},
|
|
{
|
|
hideLabels: true,
|
|
}
|
|
);
|
|
|
|
var allocated_display = makeProgressBar(
|
|
build_line.allocated,
|
|
build_line.quantity,
|
|
);
|
|
|
|
var stock_input = constructField(
|
|
`items_stock_item_${pk}`,
|
|
{
|
|
type: 'related field',
|
|
required: 'true',
|
|
},
|
|
{
|
|
hideLabels: true,
|
|
}
|
|
);
|
|
|
|
var html = `
|
|
<tr id='items_${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 < line_items.length; idx++) {
|
|
let item = line_items[idx];
|
|
|
|
// Ignore "consumable" BOM items
|
|
if (item.part_detail.consumable) {
|
|
continue;
|
|
}
|
|
|
|
var required = item.quantity || 0;
|
|
var allocated = item.allocated || 0;
|
|
var remaining = required - allocated;
|
|
|
|
if (remaining < 0) {
|
|
remaining = 0;
|
|
}
|
|
|
|
// Ensure the quantity sent to the form field is correctly formatted
|
|
remaining = formatDecimal(remaining, 15);
|
|
|
|
// We only care about entries which are not yet fully allocated
|
|
if (remaining > 0) {
|
|
table_entries += renderBuildLineRow(item, remaining);
|
|
}
|
|
}
|
|
|
|
if (table_entries.length == 0) {
|
|
|
|
showAlertDialog(
|
|
'{% jstrans "All Parts Allocated" %}',
|
|
'{% jstrans "All selected parts have been fully allocated" %}',
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
var html = ``;
|
|
|
|
// Render a "source location" input
|
|
html += constructField(
|
|
'take_from',
|
|
{
|
|
type: 'related field',
|
|
label: '{% jstrans "Source Location" %}',
|
|
help_text: '{% jstrans "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>{% jstrans "Part" %}</th>
|
|
<th>{% jstrans "Allocated" %}</th>
|
|
<th style='min-width: 250px;'>{% jstrans "Stock Item" %}</th>
|
|
<th>{% jstrans "Quantity" %}</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${table_entries}
|
|
</tbody>
|
|
</table>
|
|
`;
|
|
|
|
constructForm(`{% url "api-build-list" %}${build_id}/allocate/`, {
|
|
method: 'POST',
|
|
fields: {},
|
|
preFormContent: html,
|
|
title: '{% jstrans "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 '{% jstrans "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
|
|
line_items.forEach(function(line_item) {
|
|
initializeRelatedField(
|
|
{
|
|
name: `items_stock_item_${line_item.pk}`,
|
|
api_url: '{% url "api-stock-list" %}',
|
|
filters: {
|
|
bom_item: line_item.bom_item_detail.pk,
|
|
in_stock: true,
|
|
available: true,
|
|
part_detail: true,
|
|
location_detail: true,
|
|
},
|
|
model: 'stockitem',
|
|
required: true,
|
|
render_part_detail: true,
|
|
render_location_detail: true,
|
|
render_pk: false,
|
|
render_available_quantity: true,
|
|
auto_fill: true,
|
|
auto_fill_filters: auto_fill_filters,
|
|
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((line_item.quantity || 0) - (line_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_${line_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 '{% jstrans "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(`#items_${pk}`).remove();
|
|
});
|
|
},
|
|
onSubmit: function(fields, opts) {
|
|
|
|
// Extract elements from the form
|
|
var data = {
|
|
items: []
|
|
};
|
|
|
|
var item_pk_values = [];
|
|
|
|
line_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({
|
|
build_line: 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;
|
|
}
|
|
}
|
|
}
|
|
);
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
/**
|
|
* Automatically allocate stock items to a build
|
|
*/
|
|
function autoAllocateStockToBuild(build_id, bom_items=[], options={}) {
|
|
|
|
var html = `
|
|
<div class='alert alert-block alert-info'>
|
|
<strong>{% jstrans "Automatic Stock Allocation" %}</strong><br>
|
|
{% jstrans "Stock items will be automatically allocated to this build order, according to the provided guidelines" %}:
|
|
<ul>
|
|
<li>{% jstrans "If a location is specified, stock will only be allocated from that location" %}</li>
|
|
<li>{% jstrans "If stock is considered interchangeable, it will be allocated from the first location it is found" %}</li>
|
|
<li>{% jstrans "If substitute stock is allowed, it will be used where stock of the primary part cannot be found" %}</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
|
|
var fields = {
|
|
location: {
|
|
value: options.location,
|
|
filters: {
|
|
structural: false,
|
|
},
|
|
tree_picker: {
|
|
url: '{% url "api-location-tree" %}',
|
|
default_icon: global_settings.STOCK_LOCATION_DEFAULT_ICON,
|
|
},
|
|
},
|
|
exclude_location: {},
|
|
interchangeable: {
|
|
value: true,
|
|
},
|
|
substitutes: {
|
|
value: true,
|
|
},
|
|
optional_items: {
|
|
value: false,
|
|
},
|
|
};
|
|
|
|
constructForm(`{% url "api-build-list" %}${build_id}/auto-allocate/`, {
|
|
method: 'POST',
|
|
fields: fields,
|
|
title: '{% jstrans "Allocate Stock Items" %}',
|
|
confirm: true,
|
|
preFormContent: html,
|
|
onSuccess: function(response) {
|
|
if (options.onSuccess) {
|
|
options.onSuccess(response);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Display a table of Build orders
|
|
*/
|
|
function loadBuildTable(table, options) {
|
|
|
|
// Ensure the table starts in a known state
|
|
$(table).bootstrapTable('destroy');
|
|
|
|
var params = options.params || {};
|
|
|
|
params['part_detail'] = true;
|
|
|
|
var filters = loadTableFilters('build', params);
|
|
|
|
var calendar = null;
|
|
|
|
var filterTarget = options.filterTarget || null;
|
|
|
|
setupFilterList('build', table, filterTarget, {
|
|
download: true,
|
|
report: {
|
|
url: '{% url "api-build-report-list" %}',
|
|
key: 'build',
|
|
}
|
|
});
|
|
|
|
// Which display mode to use for the build table?
|
|
var display_mode = inventreeLoad('build-table-display-mode', 'list');
|
|
var tree_enable = display_mode == 'tree';
|
|
|
|
var loaded_calendar = false;
|
|
|
|
// Function for rendering BuildOrder calendar display
|
|
function buildEvents(calendar) {
|
|
var start = startDate(calendar);
|
|
var end = endDate(calendar);
|
|
|
|
clearEvents(calendar);
|
|
|
|
// Extract current filters from table
|
|
var table_options = $(table).bootstrapTable('getOptions');
|
|
var filters = table_options.query_params || {};
|
|
|
|
filters.min_date = start;
|
|
filters.max_date = end;
|
|
filters.part_detail = true;
|
|
|
|
// Request build orders from the server within specified date range
|
|
inventreeGet(
|
|
'{% url "api-build-list" %}',
|
|
filters,
|
|
{
|
|
success: function(response) {
|
|
|
|
for (var idx = 0; idx < response.length; idx++) {
|
|
|
|
var order = response[idx];
|
|
|
|
var date = order.creation_date;
|
|
|
|
if (order.completion_date) {
|
|
date = order.completion_date;
|
|
} else if (order.target_date) {
|
|
date = order.target_date;
|
|
}
|
|
|
|
var title = `${order.reference}`;
|
|
|
|
var color = '#4c68f5';
|
|
|
|
if (order.completed) {
|
|
color = '#25c234';
|
|
} else if (order.overdue) {
|
|
color = '#c22525';
|
|
}
|
|
|
|
var event = {
|
|
title: title,
|
|
start: date,
|
|
end: date,
|
|
url: `/build/${order.pk}/`,
|
|
backgroundColor: color,
|
|
};
|
|
|
|
calendar.addEvent(event);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
$(table).inventreeTable({
|
|
method: 'get',
|
|
formatNoMatches: function() {
|
|
return '{% jstrans "No builds matching query" %}';
|
|
},
|
|
url: '{% url "api-build-list" %}',
|
|
queryParams: filters,
|
|
groupBy: false,
|
|
sidePagination: 'server',
|
|
name: 'builds',
|
|
original: params,
|
|
treeEnable: tree_enable,
|
|
uniqueId: 'pk',
|
|
rootParentId: options.parentBuild || null,
|
|
idField: 'pk',
|
|
parentIdField: 'parent',
|
|
treeShowField: tree_enable ? 'reference' : null,
|
|
showColumns: display_mode == 'list' || display_mode == 'tree',
|
|
showCustomView: display_mode == 'calendar',
|
|
showCustomViewButton: false,
|
|
disablePagination: display_mode == 'calendar',
|
|
search: display_mode != 'calendar',
|
|
buttons: constructOrderTableButtons({
|
|
prefix: 'build',
|
|
callback: function() {
|
|
// Force complete reload of the table
|
|
loadBuildTable(table, options);
|
|
}
|
|
}),
|
|
columns: [
|
|
{
|
|
field: 'pk',
|
|
title: 'ID',
|
|
visible: false,
|
|
switchable: false,
|
|
},
|
|
{
|
|
checkbox: true,
|
|
title: '{% jstrans "Select" %}',
|
|
searchable: false,
|
|
switchable: false,
|
|
},
|
|
{
|
|
field: 'reference',
|
|
title: '{% jstrans "Build" %}',
|
|
sortable: true,
|
|
switchable: true,
|
|
formatter: function(value, row) {
|
|
|
|
var html = renderLink(value, '/build/' + row.pk + '/');
|
|
|
|
if (row.overdue) {
|
|
html += makeIconBadge('fa-calendar-times icon-red', '{% jstrans "Build order is overdue" %}');
|
|
}
|
|
|
|
return html;
|
|
}
|
|
},
|
|
{
|
|
field: 'title',
|
|
title: '{% jstrans "Description" %}',
|
|
switchable: true,
|
|
},
|
|
{
|
|
field: 'project_code',
|
|
title: '{% jstrans "Project Code" %}',
|
|
sortable: true,
|
|
switchable: global_settings.PROJECT_CODES_ENABLED,
|
|
visible: global_settings.PROJECT_CODES_ENABLED,
|
|
formatter: function(value, row) {
|
|
if (row.project_code_detail) {
|
|
return `<span title='${row.project_code_detail.description}'>${row.project_code_detail.code}</span>`;
|
|
}
|
|
}
|
|
},
|
|
{
|
|
field: 'priority',
|
|
title: '{% jstrans "Priority" %}',
|
|
switchable: true,
|
|
sortable: true,
|
|
},
|
|
{
|
|
field: 'part',
|
|
title: '{% jstrans "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: 'completed',
|
|
title: '{% jstrans "Progress" %}',
|
|
sortable: true,
|
|
formatter: function(value, row) {
|
|
return makeProgressBar(
|
|
row.completed,
|
|
row.quantity,
|
|
{
|
|
// style: 'max',
|
|
}
|
|
);
|
|
}
|
|
},
|
|
{
|
|
field: 'status',
|
|
title: '{% jstrans "Status" %}',
|
|
sortable: true,
|
|
formatter: function(value) {
|
|
return buildStatusDisplay(value);
|
|
},
|
|
},
|
|
{
|
|
field: 'creation_date',
|
|
title: '{% jstrans "Created" %}',
|
|
sortable: true,
|
|
formatter: function(value) {
|
|
return renderDate(value);
|
|
}
|
|
},
|
|
{
|
|
field: 'issued_by',
|
|
title: '{% jstrans "Issued by" %}',
|
|
sortable: true,
|
|
formatter: function(value, row) {
|
|
if (value) {
|
|
return row.issued_by_detail.username;
|
|
} else {
|
|
return `<i>{% jstrans "No user information" %}</i>`;
|
|
}
|
|
}
|
|
},
|
|
{
|
|
field: 'responsible',
|
|
title: '{% jstrans "Responsible" %}',
|
|
sortable: true,
|
|
formatter: function(value, row) {
|
|
if (!row.responsible_detail) {
|
|
return '-';
|
|
}
|
|
|
|
var html = row.responsible_detail.name;
|
|
|
|
if (row.responsible_detail.label == '{% jstrans "group" %}') {
|
|
html += `<span class='float-right fas fa-users'></span>`;
|
|
} else {
|
|
html += `<span class='float-right fas fa-user'></span>`;
|
|
}
|
|
|
|
return html;
|
|
}
|
|
},
|
|
{
|
|
field: 'target_date',
|
|
title: '{% jstrans "Target Date" %}',
|
|
sortable: true,
|
|
formatter: function(value) {
|
|
return renderDate(value);
|
|
}
|
|
},
|
|
{
|
|
field: 'completion_date',
|
|
title: '{% jstrans "Completion Date" %}',
|
|
sortable: true,
|
|
formatter: function(value) {
|
|
return renderDate(value);
|
|
}
|
|
},
|
|
],
|
|
customView: function(data) {
|
|
return `<div id='build-order-calendar'></div>`;
|
|
},
|
|
onLoadSuccess: function() {
|
|
|
|
if (tree_enable) {
|
|
$(table).treegrid({
|
|
treeColumn: 1,
|
|
});
|
|
|
|
$(table).treegrid('expandAll');
|
|
} else if (display_mode == 'calendar') {
|
|
|
|
if (!loaded_calendar) {
|
|
loaded_calendar = true;
|
|
|
|
let el = document.getElementById('build-order-calendar');
|
|
|
|
calendar = new FullCalendar.Calendar(el, {
|
|
initialView: 'dayGridMonth',
|
|
nowIndicator: true,
|
|
aspectRatio: 2.5,
|
|
locale: options.locale,
|
|
datesSet: function() {
|
|
buildEvents(calendar);
|
|
}
|
|
});
|
|
|
|
calendar.render();
|
|
} else {
|
|
calendar.render();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Render a table of BuildItem objects, which are allocated against a particular BuildLine
|
|
*/
|
|
function renderBuildLineAllocationTable(element, build_line, options={}) {
|
|
|
|
let output = options.output || 'untracked';
|
|
let tableId = `allocation-table-${output}-${build_line.pk}`;
|
|
|
|
// Construct a table element
|
|
let html = `
|
|
<div class='sub-table'>
|
|
<table class='table table-condensed table-striped' id='${tableId}'></table>
|
|
</div>`;
|
|
|
|
element.html(html);
|
|
|
|
let sub_table = $(`#${tableId}`);
|
|
|
|
// Load the allocation items into the table
|
|
sub_table.bootstrapTable({
|
|
data: build_line.allocations,
|
|
showHeader: false,
|
|
columns: [
|
|
{
|
|
field: 'part',
|
|
title: '{% jstrans "Part" %}',
|
|
formatter: function(_value, row) {
|
|
let html = imageHoverIcon(row.part_detail.thumbnail);
|
|
html += renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
|
|
return html;
|
|
}
|
|
},
|
|
{
|
|
field: 'quantity',
|
|
title: '{% jstrans "Allocated Quantity" %}',
|
|
formatter: function(_value, row) {
|
|
let text = '';
|
|
let url = '';
|
|
let serial = row.serial;
|
|
|
|
if (row.stock_item_detail) {
|
|
serial = row.stock_item_detail.serial;
|
|
}
|
|
|
|
if (serial && row.quantity == 1) {
|
|
text = `{% jstrans "Serial Number" %}: ${serial}`;
|
|
} else {
|
|
text = `{% jstrans "Quantity" %}: ${row.quantity}`;
|
|
if (row.part_detail && row.part_detail.units) {
|
|
text += ` <small>${row.part_detail.units}</small>`;
|
|
}
|
|
}
|
|
|
|
var pk = row.stock_item || row.pk;
|
|
|
|
url = `/stock/item/${pk}/`;
|
|
|
|
return renderLink(text, url);
|
|
}
|
|
},
|
|
{
|
|
field: 'location',
|
|
title: '{% jstrans "Location" %}',
|
|
formatter: function(value, row) {
|
|
if (row.location_detail) {
|
|
let text = shortenString(row.location_detail.pathstring);
|
|
let url = `/stock/location/${row.location_detail.pk}/`;
|
|
|
|
return renderLink(text, url);
|
|
} else {
|
|
return '<i>{% jstrans "No location set" %}</i>';
|
|
}
|
|
}
|
|
},
|
|
{
|
|
field: 'actions',
|
|
title: '',
|
|
formatter: function(value, row) {
|
|
let buttons = '';
|
|
buttons += makeEditButton('button-allocation-edit', row.pk, '{% jstrans "Edit stock allocation" %}');
|
|
buttons += makeDeleteButton('button-allocation-delete', row.pk, '{% jstrans "Delete stock allocation" %}');
|
|
return wrapButtons(buttons);
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
// Callbacks
|
|
$(sub_table).on('click', '.button-allocation-edit', function() {
|
|
let pk = $(this).attr('pk');
|
|
|
|
constructForm(`{% url "api-build-item-list" %}${pk}/`, {
|
|
fields: {
|
|
quantity: {},
|
|
},
|
|
title: '{% jstrans "Edit Allocation" %}',
|
|
onSuccess: function() {
|
|
$(options.parent_table).bootstrapTable('refresh');
|
|
},
|
|
});
|
|
});
|
|
|
|
$(sub_table).on('click', '.button-allocation-delete', function() {
|
|
let pk = $(this).attr('pk');
|
|
|
|
constructForm(`{% url "api-build-item-list" %}${pk}/`, {
|
|
method: 'DELETE',
|
|
title: '{% jstrans "Remove Allocation" %}',
|
|
onSuccess: function() {
|
|
$(options.parent_table).bootstrapTable('refresh');
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
/*
|
|
* Load a table of BuildLine objects associated with a Build
|
|
*
|
|
* @param {int} build_id - The ID of the Build object
|
|
* @param {object} options - Options for the table
|
|
*/
|
|
function loadBuildLineTable(table, build_id, options={}) {
|
|
|
|
let name = 'build-lines';
|
|
let params = options.params || {};
|
|
let output = options.output;
|
|
|
|
params.build = build_id;
|
|
|
|
if (output) {
|
|
params.output = output;
|
|
name += `-${output}`;
|
|
} else {
|
|
// Default to untracked parts for the build
|
|
params.tracked = false;
|
|
}
|
|
|
|
let filters = loadTableFilters('buildlines', params);
|
|
let filterTarget = options.filterTarget || '#filter-list-buildlines';
|
|
|
|
// If data is passed directly to this function, do not setup filters
|
|
if (!options.data) {
|
|
setupFilterList('buildlines', $(table), filterTarget, {
|
|
labels: {
|
|
url: '{% url "api-buildline-label-list" %}',
|
|
key: 'line',
|
|
},
|
|
singular_name: '{% jstrans "build line" %}',
|
|
plural_name: '{% jstrans "build lines" %}',
|
|
});
|
|
}
|
|
|
|
let table_options = {
|
|
name: name,
|
|
uniqueId: 'pk',
|
|
detailView: true,
|
|
detailFilter: function(index, row) {
|
|
// Detail view is available if there is any allocated stock
|
|
return row.allocated > 0;
|
|
},
|
|
detailFormatter: function(_index, row, element) {
|
|
renderBuildLineAllocationTable(element, row, {
|
|
parent_table: table,
|
|
});
|
|
},
|
|
formatNoMatches: function() {
|
|
return '{% jstrans "No build lines found" %}';
|
|
},
|
|
columns: [
|
|
{
|
|
checkbox: true,
|
|
title: '{% jstrans "Select" %}',
|
|
searchable: false,
|
|
switchable: false,
|
|
},
|
|
{
|
|
field: 'bom_item',
|
|
title: '{% jstrans "Required Part" %}',
|
|
switchable: false,
|
|
sortable: true,
|
|
sortName: 'part',
|
|
formatter: function(value, row) {
|
|
if (value == null) {
|
|
return `BOM item deleted`;
|
|
}
|
|
|
|
let html = '';
|
|
|
|
// Part thumbnail
|
|
html += imageHoverIcon(row.part_detail.thumbnail) + renderLink(row.part_detail.full_name, `/part/${row.part_detail.pk}/`);
|
|
|
|
if (row.bom_item_detail.allow_variants) {
|
|
html += makeIconBadge('fa-sitemap', '{% jstrans "Variant stock allowed" %}');
|
|
}
|
|
|
|
if (row.part_detail.trackable) {
|
|
html += makeIconBadge('fa-directions', '{% jstrans "Trackable part" %}');
|
|
}
|
|
|
|
return html;
|
|
}
|
|
},
|
|
{
|
|
field: 'reference',
|
|
title: '{% jstrans "Reference" %}',
|
|
sortable: true,
|
|
formatter: function(value, row) {
|
|
return row.bom_item_detail.reference;
|
|
}
|
|
},
|
|
{
|
|
field: 'consumable',
|
|
title: '{% jstrans "Consumable" %}',
|
|
sortable: true,
|
|
switchable: true,
|
|
formatter: function(value, row) {
|
|
return yesNoLabel(row.bom_item_detail.consumable);
|
|
}
|
|
},
|
|
{
|
|
field: 'optional',
|
|
title: '{% jstrans "Optional" %}',
|
|
sortable: true,
|
|
switchable: true,
|
|
formatter: function(value, row) {
|
|
return yesNoLabel(row.bom_item_detail.optional);
|
|
}
|
|
},
|
|
{
|
|
field: 'unit_quantity',
|
|
sortable: true,
|
|
title: '{% jstrans "Unit Quantity" %}',
|
|
formatter: function(value, row) {
|
|
let text = row.bom_item_detail.quantity;
|
|
|
|
if (row.bom_item_detail.overage) {
|
|
text += ` <span class='badge bg-dark rounded-pill badge-right'>(+${row.bom_item_detail.overage})</span>`;
|
|
}
|
|
|
|
if (row.part_detail.units) {
|
|
text += ` <small> [${row.part_detail.units}]</small>`;
|
|
}
|
|
|
|
return text;
|
|
}
|
|
},
|
|
{
|
|
field: 'quantity',
|
|
title: '{% jstrans "Required Quantity" %}',
|
|
sortable: true,
|
|
},
|
|
{
|
|
field: 'available_stock',
|
|
title: '{% jstrans "Available" %}',
|
|
sortable: true,
|
|
formatter: function(value, row) {
|
|
var url = `/part/${row.part_detail.pk}/?display=part-stock`;
|
|
// Calculate the "available" quantity
|
|
let available = row.available_stock + row.available_substitute_stock;
|
|
|
|
if (row.bom_item_detail.allow_variants) {
|
|
available += row.available_variant_stock;
|
|
}
|
|
|
|
let text = '';
|
|
|
|
if (available > 0) {
|
|
text += `${formatDecimal(available)}`;
|
|
|
|
if (row.part_detail.units) {
|
|
text += ` <small>[${row.part_detail.units}]</small>`;
|
|
}
|
|
}
|
|
|
|
let icons = '';
|
|
|
|
if (row.bom_item_detail.consumable) {
|
|
icons += `<span class='fas fa-info-circle icon-blue float-right' title='{% jstrans "Consumable item" %}'></span>`;
|
|
} else {
|
|
if (available < (row.quantity - row.allocated)) {
|
|
icons += makeIconBadge('fa-times-circle icon-red', '{% jstrans "Insufficient stock available" %}');
|
|
} else {
|
|
icons += makeIconBadge('fa-check-circle icon-green', '{% jstrans "Sufficient stock available" %}');
|
|
}
|
|
|
|
if (available <= 0) {
|
|
icons += `<span class='badge rounded-pill bg-danger'>{% jstrans "No Stock Available" %}</span>`;
|
|
} else {
|
|
let extra = '';
|
|
if ((row.available_substitute_stock > 0) && (row.available_variant_stock > 0)) {
|
|
extra = '{% jstrans "Includes variant and substitute stock" %}';
|
|
} else if (row.available_variant_stock > 0) {
|
|
extra = '{% jstrans "Includes variant stock" %}';
|
|
} else if (row.available_substitute_stock > 0) {
|
|
extra = '{% jstrans "Includes substitute stock" %}';
|
|
}
|
|
|
|
if (extra) {
|
|
icons += makeIconBadge('fa-info-circle icon-blue', extra);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (row.on_order && row.on_order > 0) {
|
|
icons += makeIconBadge('fa-shopping-cart', `{% jstrans "On Order" %}: ${formatDecimal(row.on_order)}`);
|
|
}
|
|
|
|
return renderLink(text, url) + icons;
|
|
}
|
|
},
|
|
{
|
|
field: 'allocated',
|
|
title: '{% jstrans "Allocated" %}',
|
|
sortable: true,
|
|
formatter: function(value, row) {
|
|
return makeProgressBar(row.allocated, row.quantity);
|
|
}
|
|
},
|
|
{
|
|
field: 'actions',
|
|
title: '',
|
|
switchable: false,
|
|
sortable: false,
|
|
formatter: function(value, row) {
|
|
let buttons = '';
|
|
let pk = row.pk;
|
|
|
|
// Consumable items do not need to be allocated
|
|
if (row.bom_item_detail.consumable) {
|
|
return `<em>{% jstrans "Consumable Item" %}</em>`;
|
|
}
|
|
|
|
if (row.part_detail.trackable && !options.output) {
|
|
// Tracked parts must be allocated to a specific build output
|
|
return `<em>{% jstrans "Tracked item" %}</em>`;
|
|
}
|
|
|
|
if (row.allocated < row.quantity) {
|
|
|
|
// Add a button to "build" stock for this line
|
|
if (row.part_detail.assembly) {
|
|
buttons += makeIconButton('fa-tools icon-blue', 'button-build', pk, '{% jstrans "Build stock" %}');
|
|
}
|
|
|
|
// Add a button to "purchase" stock for this line
|
|
if (row.part_detail.purchaseable) {
|
|
buttons += makeIconButton('fa-shopping-cart icon-blue', 'button-buy', pk, '{% jstrans "Order stock" %}');
|
|
}
|
|
|
|
// Add a button to "allocate" stock for this line
|
|
buttons += makeIconButton('fa-sign-in-alt icon-green', 'button-allocate', pk, '{% jstrans "Allocate stock" %}');
|
|
}
|
|
|
|
if (row.allocated > 0) {
|
|
buttons += makeRemoveButton('button-unallocate', pk, '{% jstrans "Remove stock allocation" %}');
|
|
}
|
|
|
|
return wrapButtons(buttons);
|
|
}
|
|
}
|
|
]
|
|
};
|
|
|
|
if (options.data) {
|
|
Object.assign(table_options, {
|
|
data: options.data,
|
|
sidePagination: 'client',
|
|
showColumns: false,
|
|
pagination: false,
|
|
disablePagination: true,
|
|
search: false,
|
|
});
|
|
} else {
|
|
Object.assign(table_options, {
|
|
url: '{% url "api-build-line-list" %}',
|
|
queryParams: filters,
|
|
original: params,
|
|
search: true,
|
|
sidePagination: 'server',
|
|
pagination: true,
|
|
showColumns: true,
|
|
buttons: constructExpandCollapseButtons(table),
|
|
});
|
|
}
|
|
|
|
$(table).inventreeTable(table_options);
|
|
|
|
/* Add callbacks for allocation buttons */
|
|
|
|
// Callback to build stock
|
|
$(table).on('click', '.button-build', function() {
|
|
let pk = $(this).attr('pk');
|
|
let row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
|
|
|
// Start a new "build" for this line
|
|
newBuildOrder({
|
|
part: row.part_detail.pk,
|
|
parent: build_id,
|
|
quantity: Math.max(row.quantity - row.allocated, 0),
|
|
});
|
|
});
|
|
|
|
// Callback to purchase stock
|
|
$(table).on('click', '.button-buy', function() {
|
|
let pk = $(this).attr('pk');
|
|
let row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
|
|
|
// TODO: Refresh table after purchase order is created
|
|
orderParts([row.part_detail], {});
|
|
});
|
|
|
|
// Callback to allocate stock
|
|
$(table).on('click', '.button-allocate', function() {
|
|
let pk = $(this).attr('pk');
|
|
let row = $(table).bootstrapTable('getRowByUniqueId', pk);
|
|
|
|
allocateStockToBuild(build_id, [row], {
|
|
output: options.output,
|
|
success: function() {
|
|
$(table).bootstrapTable('refresh');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Callback to un-allocate stock
|
|
$(table).on('click', '.button-unallocate', function() {
|
|
let pk = $(this).attr('pk');
|
|
|
|
deallocateStock(build_id, {
|
|
build_line: pk,
|
|
output: output,
|
|
onSuccess: function() {
|
|
$(table).bootstrapTable('refresh');
|
|
}
|
|
});
|
|
});
|
|
}
|