2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-18 13:05:42 +00:00

[Feature] Scrap Build Outputs (#4800)

* Update docs for status codes

* Adds API endpoint for scrapping individual build outputs

* Support 'buildorder' reference in stock tracking history

* Add page for build output documentation

* Build docs

* Add example build order process to docs

* remove debug statement

* JS lint cleanup

* Add migration file for stock status

* Add unit tests for build output scrapping

* Increment API version

* bug fix
This commit is contained in:
Oliver
2023-05-13 22:19:35 +10:00
committed by GitHub
parent 634daa2161
commit b2ceac2c4a
39 changed files with 794 additions and 254 deletions

View File

@ -397,9 +397,17 @@ function makeBuildOutputButtons(output_id, build_info, options={}) {
'{% trans "Complete build output" %}',
);
// Add a button to "delete" this build output
// Add a button to "scrap" the build output
html += makeIconButton(
'fa-times-circle icon-red',
'button-output-scrap',
output_id,
'{% trans "Scrap build output" %}',
);
// Add a button to "remove" this build output
html += makeDeleteButton(
'button-output-delete',
'button-output-remove',
output_id,
'{% trans "Delete build output" %}',
);
@ -452,6 +460,51 @@ function unallocateStock(build_id, options={}) {
}
/*
* Helper function to render a single build output in a modal form
*/
function renderBuildOutput(output, opts={}) {
let pk = output.pk;
let 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}`;
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, '{% trans "Remove row" %}');
buttons += '</div>';
let field = constructField(
`outputs_output_${pk}`,
{
type: 'raw',
html: output_html,
},
{
hideLabels: true,
}
);
let html = `
<tr id='output_row_${pk}'>
<td>${field}</td>
<td>${output.part_detail.full_name}</td>
<td>${buttons}</td>
</tr>`;
return html;
}
/**
* Launch a modal form to complete selected build outputs
*/
@ -465,48 +518,6 @@ function completeBuildOutputs(build_id, outputs, options={}) {
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}`;
if (output.part_detail && output.part_detail.units) {
output_html += ` ${output.part_detail.units} `;
}
}
var buttons = `<div class='btn-group float-right' role='group'>`;
buttons += makeRemoveButton('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 = '';
@ -515,6 +526,9 @@ function completeBuildOutputs(build_id, outputs, options={}) {
});
var html = `
<div class='alert alert-block alert-success'>
{% trans "Selected build outputs will be marked as complete" %}
</div>
<table class='table table-striped table-condensed' id='build-complete-table'>
<thead>
<th colspan='2'>{% trans "Output" %}</th>
@ -613,8 +627,122 @@ function completeBuildOutputs(build_id, outputs, options={}) {
/*
* 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(
'{% trans "Select Build Outputs" %}',
'{% trans "At least one build output must be selected" %}',
);
return;
}
let table_entries = '';
outputs.forEach(function(output) {
table_entries += renderBuildOutput(output);
});
var html = `
<div class='alert alert-block alert-danger'>
{% trans "Selected build outputs will be marked as scrapped" %}
<ul>
<li>{% trans "Scrapped output are given the 'rejected' status" %}</li>
<li>{% trans "Allocated stock items will no longer be available" %}</li>
<li>{% trans "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'>{% trans "Output" %}</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: {},
notes: {},
discard_allocations: {},
},
confirm: true,
title: '{% trans "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}`);
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;
}
}
}
);
}
});
}
/**
* Launch a modal form to delete selected build outputs
* Launch a modal form to delete selected build outputs.
* Deleted outputs are expunged from the database.
*/
function deleteBuildOutputs(build_id, outputs, options={}) {
@ -626,48 +754,6 @@ function deleteBuildOutputs(build_id, outputs, options={}) {
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}`;
if (output.part_detail && output.part_detail.units) {
output_html += ` ${output.part_detail.units} `;
}
}
var buttons = `<div class='btn-group float-right' role='group'>`;
buttons += makeRemoveButton('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 = '';
@ -676,6 +762,13 @@ function deleteBuildOutputs(build_id, outputs, options={}) {
});
var html = `
<div class='alert alert-block alert-danger'>
{% trans "Selected build outputs will be deleted" %}
<ul>
<li>{% trans "Build output data will be permanently deleted" %}</li>
<li>{% trans "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'>{% trans "Output" %}</th>
@ -952,8 +1045,25 @@ function loadBuildOutputTable(build_info, options={}) {
);
});
// Callback for the "delete" button
$(table).find('.button-output-delete').click(function() {
// Callback for the "scrap" button
$(table).find('.button-output-scrap').click(function() {
var pk = $(this).attr('pk');
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
scrapBuildOutputs(
build_info.pk,
[output],
{
success: function() {
$(table).bootstrapTable('refresh');
$('#build-stock-table').bootstrapTable('refresh');
}
}
);
});
// Callback for the "remove" button
$(table).find('.button-output-remove').click(function() {
var pk = $(this).attr('pk');
var output = $(table).bootstrapTable('getRowByUniqueId', pk);
@ -1368,6 +1478,25 @@ function loadBuildOutputTable(build_info, options={}) {
// Add callbacks for the various table menubar buttons
// Scrap multiple outputs
$('#multi-output-scrap').click(function() {
var outputs = getTableData(table);
scrapBuildOutputs(
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');
}
}
);
});
// Complete multiple outputs
$('#multi-output-complete').click(function() {
var outputs = getTableData(table);

View File

@ -3,8 +3,6 @@
{% load status_codes %}
/* globals
attachSelect,
closeModal,
constructField,
constructFormBody,
getFormFieldValue,
@ -18,12 +16,6 @@
loadTableFilters,
makeIconBadge,
makeIconButton,
makeOptionsList,
modalEnable,
modalSetContent,
modalSetTitle,
modalSubmit,
openModal,
renderLink,
scanItemsIntoLocation,
showAlertDialog,
@ -54,7 +46,6 @@
serializeStockItem,
stockItemFields,
stockLocationFields,
stockStatusCodes,
uninstallStockItem,
*/
@ -603,22 +594,6 @@ function findStockItemBySerialNumber(part_id) {
}
/* Stock API functions
* Requires api.js to be loaded first
*/
function stockStatusCodes() {
return [
{% for code in StockStatus.list %}
{
key: {{ code.key }},
text: '{{ code.value }}',
},
{% endfor %}
];
}
/**
* Assign multiple stock items to a customer
*/
@ -2261,93 +2236,6 @@ function loadStockTable(table, options) {
orderParts(parts, {});
});
$('#multi-item-set-status').click(function() {
// Select and set the STATUS field for selected stock items
var selections = getTableData(table);
// Select stock status
var modal = '#modal-form';
var status_list = makeOptionsList(
stockStatusCodes(),
function(item) {
return item.text;
},
function(item) {
return item.key;
}
);
// Add an empty option at the start of the list
status_list.unshift('<option value="">---------</option>');
// Construct form
var html = `
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
<div class='form-group'>
<label class='control-label requiredField' for='id_status'>
{% trans "Stock Status" %}
</label>
<div class='controls'>
<select id='id_status' class='select form-control' name='label'>
${status_list}
</select>
</div>
</div>
</form>`;
openModal({
modal: modal,
});
modalEnable(modal, true);
modalSetTitle(modal, '{% trans "Set Stock Status" %}');
modalSetContent(modal, html);
attachSelect(modal);
modalSubmit(modal, function() {
var label = $(modal).find('#id_status');
var status_code = label.val();
closeModal(modal);
if (!status_code) {
showAlertDialog(
'{% trans "Select Status Code" %}',
'{% trans "Status code must be selected" %}'
);
return;
}
var requests = [];
selections.forEach(function(item) {
var url = `/api/stock/${item.pk}/`;
requests.push(
inventreePut(
url,
{
status: status_code,
},
{
method: 'PATCH',
success: function() {
}
}
)
);
});
$.when.apply($, requests).done(function() {
$(table).bootstrapTable('refresh');
});
});
});
$('#multi-item-delete').click(function() {
var selections = getTableData(table);
@ -2697,11 +2585,24 @@ function loadStockTrackingTable(table, options) {
html += '</td></tr>';
}
// BuildOrder Information
if (details.buildorder) {
html += `<tr><th>{% trans "Build Order" %}</th>`;
html += `<td>`;
if (details.buildorder_detail) {
html += renderLink(
details.buildorder_detail.reference,
`/build/${details.buildorder}/`
);
} else {
html += `<i>{% trans "Build order no longer exists" %}</i>`;
}
}
// PurchaseOrder Information
if (details.purchaseorder) {
html += `<tr><th>{% trans "Purchase Order" %}</th>`;
html += '<td>';
if (details.purchaseorder_detail) {

View File

@ -405,6 +405,12 @@ function getStockTestTableFilters() {
}
// Return a dictionary of filters for the "stocktracking" table
function getStockTrackingTableFilters() {
return {};
}
// Return a dictionary of filters for the "part tests" table
function getPartTestTemplateFilters() {
return {
@ -741,6 +747,8 @@ function getAvailableTableFilters(tableKey) {
return getStockTableFilters();
case 'stocktests':
return getStockTestTableFilters();
case 'stocktracking':
return getStockTrackingTableFilters();
case 'supplierpart':
return getSupplierPartFilters();
case 'usedin':

View File

@ -37,7 +37,6 @@
<li><a class='dropdown-item' href='#' id='multi-item-merge' title='{% trans "Merge selected stock items" %}'><span class='fas fa-object-group'></span> {% trans "Merge stock" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-item-order' title='{% trans "Order selected items" %}'><span class='fas fa-shopping-cart'></span> {% trans "Order stock" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-item-assign' title='{% trans "Assign to customer" %}'><span class='fas fa-user-tie'></span> {% trans "Assign to customer" %}</a></li>
<li><a class='dropdown-item' href='#' id='multi-item-set-status' title='{% trans "Change status" %}'><span class='fas fa-exclamation-circle'></span> {% trans "Change stock status" %}</a></li>
{% endif %}
{% if roles.stock.delete %}
<li><a class='dropdown-item' href='#' id='multi-item-delete' title='{% trans "Delete selected items" %}'><span class='fas fa-trash-alt icon-red'></span> {% trans "Delete stock" %}</a></li>