mirror of
https://github.com/inventree/InvenTree.git
synced 2025-07-03 04:00:57 +00:00
Merge branch 'inventree:master' into fr-1421-sso
This commit is contained in:
@ -4,7 +4,6 @@
|
||||
/* globals
|
||||
buildStatusDisplay,
|
||||
constructForm,
|
||||
getFieldByName,
|
||||
global_settings,
|
||||
imageHoverIcon,
|
||||
inventreeGet,
|
||||
@ -20,6 +19,7 @@
|
||||
*/
|
||||
|
||||
/* exported
|
||||
allocateStockToBuild,
|
||||
editBuildOrder,
|
||||
loadAllocationTable,
|
||||
loadBuildOrderAllocationTable,
|
||||
@ -102,6 +102,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
|
||||
*/
|
||||
|
||||
var buildId = buildInfo.pk;
|
||||
var partId = buildInfo.part;
|
||||
|
||||
var outputId = 'untracked';
|
||||
|
||||
@ -120,11 +121,10 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
|
||||
|
||||
var html = `<div class='btn-group float-right' role='group'>`;
|
||||
|
||||
// "Auto" allocation only works for untracked stock items
|
||||
if (!output && lines > 0) {
|
||||
if (lines > 0) {
|
||||
html += makeIconButton(
|
||||
'fa-magic icon-blue', 'button-output-auto', outputId,
|
||||
'{% trans "Auto-allocate stock items to this output" %}',
|
||||
'fa-sign-in-alt icon-blue', 'button-output-auto', outputId,
|
||||
'{% trans "Allocate stock items to this build output" %}',
|
||||
);
|
||||
}
|
||||
|
||||
@ -136,7 +136,6 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (output) {
|
||||
|
||||
// Add a button to "complete" the particular build output
|
||||
@ -163,11 +162,17 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
|
||||
|
||||
// Add callbacks for the buttons
|
||||
$(panel).find(`#button-output-auto-${outputId}`).click(function() {
|
||||
|
||||
var bom_items = $(panel).find(`#allocation-table-${outputId}`).bootstrapTable('getData');
|
||||
|
||||
// Launch modal dialog to perform auto-allocation
|
||||
launchModalForm(`/build/${buildId}/auto-allocate/`,
|
||||
allocateStockToBuild(
|
||||
buildId,
|
||||
partId,
|
||||
bom_items,
|
||||
{
|
||||
data: {
|
||||
},
|
||||
source_location: buildInfo.source_location,
|
||||
output: outputId,
|
||||
success: reloadTable,
|
||||
}
|
||||
);
|
||||
@ -344,18 +349,26 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
function requiredQuantity(row) {
|
||||
// Return the requied quantity for a given row
|
||||
|
||||
var quantity = 0;
|
||||
|
||||
if (output) {
|
||||
// "Tracked" parts are calculated against individual build outputs
|
||||
return row.quantity * output.quantity;
|
||||
quantity = row.quantity * output.quantity;
|
||||
} else {
|
||||
// "Untracked" parts are specified against the build itself
|
||||
return row.quantity * buildInfo.quantity;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -365,6 +378,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
quantity += item.quantity;
|
||||
});
|
||||
|
||||
row.allocated = quantity;
|
||||
|
||||
return quantity;
|
||||
}
|
||||
|
||||
@ -377,52 +392,28 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
// Primary key of the 'sub_part'
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
// Launch form to allocate new stock against this output
|
||||
launchModalForm('{% url "build-item-create" %}', {
|
||||
success: reloadTable,
|
||||
data: {
|
||||
part: pk,
|
||||
build: buildId,
|
||||
install_into: outputId,
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'stock_item',
|
||||
label: '{% trans "New Stock Item" %}',
|
||||
title: '{% trans "Create new Stock Item" %}',
|
||||
url: '{% url "stock-item-create" %}',
|
||||
data: {
|
||||
part: 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,
|
||||
],
|
||||
callback: [
|
||||
{
|
||||
field: 'stock_item',
|
||||
action: function(value) {
|
||||
inventreeGet(
|
||||
`/api/stock/${value}/`, {},
|
||||
{
|
||||
success: function(response) {
|
||||
|
||||
// How many items are actually available for the given stock item?
|
||||
var available = response.quantity - response.allocated;
|
||||
|
||||
var field = getFieldByName('#modal-form', 'quantity');
|
||||
|
||||
// Allocation quantity initial value
|
||||
var initial = field.attr('value');
|
||||
|
||||
if (available < initial) {
|
||||
field.val(available);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
{
|
||||
source_location: buildInfo.source_location,
|
||||
success: function(data) {
|
||||
$(table).bootstrapTable('refresh');
|
||||
},
|
||||
output: output == null ? null : output.pk,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Callback for 'buy' button
|
||||
@ -623,17 +614,22 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
|
||||
var url = '';
|
||||
|
||||
if (row.serial && row.quantity == 1) {
|
||||
text = `{% trans "Serial Number" %}: ${row.serial}`;
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
{% if build.status == BuildStatus.COMPLETE %}
|
||||
url = `/stock/item/${row.pk}/`;
|
||||
{% else %}
|
||||
url = `/stock/item/${row.stock_item}/`;
|
||||
{% endif %}
|
||||
var pk = row.stock_item || row.pk;
|
||||
|
||||
url = `/stock/item/${pk}/`;
|
||||
|
||||
return renderLink(text, url);
|
||||
}
|
||||
@ -680,22 +676,31 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
// Assign button callbacks to the newly created allocation buttons
|
||||
subTable.find('.button-allocation-edit').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
launchModalForm(`/build/item/${pk}/edit/`, {
|
||||
success: reloadTable,
|
||||
|
||||
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');
|
||||
launchModalForm(`/build/item/${pk}/delete/`, {
|
||||
success: reloadTable,
|
||||
|
||||
constructForm(`/api/build/item/${pk}/`, {
|
||||
method: 'DELETE',
|
||||
title: '{% trans "Remove Allocation" %}',
|
||||
onSuccess: reloadTable,
|
||||
});
|
||||
});
|
||||
},
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
visible: false,
|
||||
visible: true,
|
||||
switchable: false,
|
||||
checkbox: true,
|
||||
},
|
||||
{
|
||||
field: 'sub_part_detail.full_name',
|
||||
@ -817,6 +822,316 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 take parts 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;
|
||||
}
|
||||
|
||||
table_entries += renderBomItemRow(bom_item, remaining);
|
||||
}
|
||||
|
||||
if (bom_items.length == 0) {
|
||||
|
||||
showAlertDialog(
|
||||
'{% trans "Select Parts" %}',
|
||||
'{% trans "You must select at least one part to allocate" %}',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var html = ``;
|
||||
|
||||
// Render a "take from" 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,
|
||||
);
|
||||
|
||||
// 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,
|
||||
part_detail: false,
|
||||
location_detail: true,
|
||||
},
|
||||
model: 'stockitem',
|
||||
required: true,
|
||||
render_part_detail: false,
|
||||
render_location_detail: true,
|
||||
auto_fill: true,
|
||||
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 callback to "clear" button for take_from field
|
||||
addClearCallback(
|
||||
'take_from',
|
||||
take_from_field,
|
||||
options,
|
||||
);
|
||||
|
||||
// Add 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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
function loadBuildTable(table, options) {
|
||||
// Display a table of Build objects
|
||||
|
||||
|
@ -273,6 +273,11 @@ function setupFilterList(tableKey, table, target) {
|
||||
|
||||
var element = $(target);
|
||||
|
||||
if (!element) {
|
||||
console.log(`WARNING: setupFilterList could not find target '${target}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
// One blank slate, please
|
||||
element.empty();
|
||||
|
||||
|
@ -728,10 +728,17 @@ function updateFieldValues(fields, options) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Update the value of a named field
|
||||
*/
|
||||
function updateFieldValue(name, value, field, options) {
|
||||
var el = $(options.modal).find(`#id_${name}`);
|
||||
|
||||
if (!el) {
|
||||
console.log(`WARNING: updateFieldValue could not find field '${name}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (field.type) {
|
||||
case 'boolean':
|
||||
el.prop('checked', value);
|
||||
@ -864,6 +871,78 @@ function clearFormErrors(options) {
|
||||
$(options.modal).find('#non-field-errors').html('');
|
||||
}
|
||||
|
||||
/*
|
||||
* Display form error messages as returned from the server,
|
||||
* specifically for errors returned in an array.
|
||||
*
|
||||
* We need to know the unique ID of each item in the array,
|
||||
* and the array length must equal the length of the array returned from the server
|
||||
*
|
||||
* arguments:
|
||||
* - response: The JSON error response from the server
|
||||
* - parent: The name of the parent field e.g. "items"
|
||||
* - options: The global options struct
|
||||
*
|
||||
* options:
|
||||
* - nested: A map of nested ID values for the "parent" field
|
||||
* e.g.
|
||||
* {
|
||||
* "items": [
|
||||
* 1,
|
||||
* 2,
|
||||
* 12
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
*/
|
||||
|
||||
function handleNestedErrors(errors, field_name, options) {
|
||||
|
||||
var error_list = errors[field_name];
|
||||
|
||||
// Ignore null or empty list
|
||||
if (!error_list) {
|
||||
return;
|
||||
}
|
||||
|
||||
var nest_list = nest_list = options['nested'][field_name];
|
||||
|
||||
// Nest list must be provided!
|
||||
if (!nest_list) {
|
||||
console.log(`WARNING: handleNestedErrors missing nesting options for field '${fieldName}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (var idx = 0; idx < error_list.length; idx++) {
|
||||
|
||||
var error_item = error_list[idx];
|
||||
|
||||
if (idx >= nest_list.length) {
|
||||
console.log(`WARNING: handleNestedErrors returned greater number of errors (${error_list.length}) than could be handled (${nest_list.length})`);
|
||||
break;
|
||||
}
|
||||
|
||||
// Extract the particular ID of the nested item
|
||||
var nest_id = nest_list[idx];
|
||||
|
||||
// Here, error_item is a map of field names to error messages
|
||||
for (sub_field_name in error_item) {
|
||||
var errors = error_item[sub_field_name];
|
||||
|
||||
// Find the target (nested) field
|
||||
var target = `${field_name}_${sub_field_name}_${nest_id}`;
|
||||
|
||||
for (var ii = errors.length-1; ii >= 0; ii--) {
|
||||
|
||||
var error_text = errors[ii];
|
||||
|
||||
addFieldErrorMessage(target, error_text, ii, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Display form error messages as returned from the server.
|
||||
@ -913,28 +992,30 @@ function handleFormErrors(errors, fields, options) {
|
||||
|
||||
for (var field_name in errors) {
|
||||
|
||||
// Add the 'has-error' class
|
||||
$(options.modal).find(`#div_id_${field_name}`).addClass('has-error');
|
||||
if (field_name in fields) {
|
||||
|
||||
var field_dom = $(options.modal).find(`#errors-${field_name}`); // $(options.modal).find(`#id_${field_name}`);
|
||||
var field = fields[field_name];
|
||||
|
||||
var field_errors = errors[field_name];
|
||||
if ((field.type == 'field') && ('child' in field)) {
|
||||
// This is a "nested" field
|
||||
handleNestedErrors(errors, field_name, options);
|
||||
} else {
|
||||
// This is a "simple" field
|
||||
|
||||
if (field_errors && !first_error_field && isFieldVisible(field_name, options)) {
|
||||
first_error_field = field_name;
|
||||
}
|
||||
var field_errors = errors[field_name];
|
||||
|
||||
// Add an entry for each returned error message
|
||||
for (var ii = field_errors.length-1; ii >= 0; ii--) {
|
||||
if (field_errors && !first_error_field && isFieldVisible(field_name, options)) {
|
||||
first_error_field = field_name;
|
||||
}
|
||||
|
||||
var error_text = field_errors[ii];
|
||||
// Add an entry for each returned error message
|
||||
for (var ii = field_errors.length-1; ii >= 0; ii--) {
|
||||
|
||||
var error_html = `
|
||||
<span id='error_${ii+1}_id_${field_name}' class='help-block form-error-message'>
|
||||
<strong>${error_text}</strong>
|
||||
</span>`;
|
||||
var error_text = field_errors[ii];
|
||||
|
||||
field_dom.append(error_html);
|
||||
addFieldErrorMessage(field_name, error_text, ii, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -952,6 +1033,30 @@ function handleFormErrors(errors, fields, options) {
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Add a rendered error message to the provided field
|
||||
*/
|
||||
function addFieldErrorMessage(field_name, error_text, error_idx, options) {
|
||||
|
||||
// Add the 'has-error' class
|
||||
$(options.modal).find(`#div_id_${field_name}`).addClass('has-error');
|
||||
|
||||
var field_dom = $(options.modal).find(`#errors-${field_name}`);
|
||||
|
||||
if (field_dom) {
|
||||
|
||||
var error_html = `
|
||||
<span id='error_${error_idx}_id_${field_name}' class='help-block form-error-message'>
|
||||
<strong>${error_text}</strong>
|
||||
</span>`;
|
||||
|
||||
field_dom.append(error_html);
|
||||
} else {
|
||||
console.log(`WARNING: addFieldErrorMessage could not locate field '${field_name}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isFieldVisible(field, options) {
|
||||
|
||||
return $(options.modal).find(`#div_id_${field}`).is(':visible');
|
||||
@ -1007,7 +1112,14 @@ function addClearCallbacks(fields, options) {
|
||||
|
||||
function addClearCallback(name, field, options) {
|
||||
|
||||
$(options.modal).find(`#clear_${name}`).click(function() {
|
||||
var el = $(options.modal).find(`#clear_${name}`);
|
||||
|
||||
if (!el) {
|
||||
console.log(`WARNING: addClearCallback could not find field '${name}'`);
|
||||
return;
|
||||
}
|
||||
|
||||
el.click(function() {
|
||||
updateFieldValue(name, null, field, options);
|
||||
});
|
||||
}
|
||||
@ -1168,7 +1280,7 @@ function addSecondaryModal(field, fields, options) {
|
||||
|
||||
|
||||
/*
|
||||
* Initializea single related-field
|
||||
* Initialize a single related-field
|
||||
*
|
||||
* argument:
|
||||
* - modal: DOM identifier for the modal window
|
||||
@ -1182,7 +1294,7 @@ function initializeRelatedField(field, fields, options) {
|
||||
|
||||
if (!field.api_url) {
|
||||
// TODO: Provide manual api_url option?
|
||||
console.log(`Related field '${name}' missing 'api_url' parameter.`);
|
||||
console.log(`WARNING: Related field '${name}' missing 'api_url' parameter.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1203,6 +1315,15 @@ function initializeRelatedField(field, fields, options) {
|
||||
placeholder: '',
|
||||
dropdownParent: $(options.modal),
|
||||
dropdownAutoWidth: false,
|
||||
language: {
|
||||
noResults: function(query) {
|
||||
if (field.noResults) {
|
||||
return field.noResults(query);
|
||||
} else {
|
||||
return '{% trans "No results found" %}';
|
||||
}
|
||||
}
|
||||
},
|
||||
ajax: {
|
||||
url: field.api_url,
|
||||
dataType: 'json',
|
||||
@ -1225,6 +1346,11 @@ function initializeRelatedField(field, fields, options) {
|
||||
query.search = params.term;
|
||||
query.offset = offset;
|
||||
query.limit = pageSize;
|
||||
|
||||
// Allow custom run-time filter augmentation
|
||||
if ('adjustFilters' in field) {
|
||||
query = field.adjustFilters(query);
|
||||
}
|
||||
|
||||
return query;
|
||||
},
|
||||
@ -1319,6 +1445,7 @@ function initializeRelatedField(field, fields, options) {
|
||||
|
||||
// If a 'value' is already defined, grab the model info from the server
|
||||
if (field.value) {
|
||||
|
||||
var pk = field.value;
|
||||
var url = `${field.api_url}/${pk}/`.replace('//', '/');
|
||||
|
||||
@ -1327,6 +1454,24 @@ function initializeRelatedField(field, fields, options) {
|
||||
setRelatedFieldData(name, data, options);
|
||||
}
|
||||
});
|
||||
} else if (field.auto_fill) {
|
||||
// Attempt to auto-fill the field
|
||||
|
||||
var filters = field.filters || {};
|
||||
|
||||
// Enforce pagination, limit to a single return (for fast query)
|
||||
filters.limit = 1;
|
||||
filters.offset = 0;
|
||||
|
||||
inventreeGet(field.api_url, field.filters || {}, {
|
||||
success: function(data) {
|
||||
|
||||
// Only a single result is available, given the provided filters
|
||||
if (data.count == 1) {
|
||||
setRelatedFieldData(name, data.results[0], options);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1370,6 +1515,7 @@ function initializeChoiceField(field, fields, options) {
|
||||
select.select2({
|
||||
dropdownAutoWidth: false,
|
||||
dropdownParent: $(options.modal),
|
||||
width: '100%',
|
||||
});
|
||||
}
|
||||
|
||||
@ -1884,7 +2030,7 @@ function constructChoiceInput(name, parameters) {
|
||||
*/
|
||||
function constructRelatedFieldInput(name) {
|
||||
|
||||
var html = `<select id='id_${name}' class='select form-control' name='${name}'></select>`;
|
||||
var html = `<select id='id_${name}' class='select form-control' name='${name}' style='width: 100%;'></select>`;
|
||||
|
||||
// Don't load any options - they will be filled via an AJAX request
|
||||
|
||||
|
@ -65,7 +65,7 @@ function imageHoverIcon(url) {
|
||||
function thumbnailImage(url) {
|
||||
|
||||
if (!url) {
|
||||
url = '/static/img/blank_img.png';
|
||||
url = blankImage();
|
||||
}
|
||||
|
||||
// TODO: Support insertion of custom classes
|
||||
|
@ -37,7 +37,7 @@ function renderCompany(name, data, parameters, options) {
|
||||
|
||||
html += `<span><b>${data.name}</b></span> - <i>${data.description}</i>`;
|
||||
|
||||
html += `<span class='float-right'>{% trans "Company ID" %}: ${data.pk}</span>`;
|
||||
html += `<span class='float-right'><small>{% trans "Company ID" %}: ${data.pk}</small></span>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -47,22 +47,59 @@ function renderCompany(name, data, parameters, options) {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function renderStockItem(name, data, parameters, options) {
|
||||
|
||||
var image = data.part_detail.thumbnail || data.part_detail.image || blankImage();
|
||||
|
||||
var html = `<img src='${image}' class='select2-thumbnail'>`;
|
||||
|
||||
html += ` <span>${data.part_detail.full_name || data.part_detail.name}</span>`;
|
||||
|
||||
if (data.serial && data.quantity == 1) {
|
||||
html += ` - <i>{% trans "Serial Number" %}: ${data.serial}`;
|
||||
} else {
|
||||
html += ` - <i>{% trans "Quantity" %}: ${data.quantity}`;
|
||||
var image = blankImage();
|
||||
|
||||
if (data.part_detail) {
|
||||
image = data.part_detail.thumbnail || data.part_detail.image || blankImage();
|
||||
}
|
||||
|
||||
if (data.part_detail.description) {
|
||||
var html = '';
|
||||
|
||||
var render_part_detail = true;
|
||||
|
||||
if ('render_part_detail' in parameters) {
|
||||
render_part_detail = parameters['render_part_detail'];
|
||||
}
|
||||
|
||||
if (render_part_detail) {
|
||||
html += `<img src='${image}' class='select2-thumbnail'>`;
|
||||
html += ` <span>${data.part_detail.full_name || data.part_detail.name}</span>`;
|
||||
}
|
||||
|
||||
html += '<span>';
|
||||
|
||||
if (data.serial && data.quantity == 1) {
|
||||
html += `{% trans "Serial Number" %}: ${data.serial}`;
|
||||
} else {
|
||||
html += `{% trans "Quantity" %}: ${data.quantity}`;
|
||||
}
|
||||
|
||||
html += '</span>';
|
||||
|
||||
if (render_part_detail && data.part_detail.description) {
|
||||
html += `<p><small>${data.part_detail.description}</small></p>`;
|
||||
}
|
||||
|
||||
var render_stock_id = true;
|
||||
|
||||
if ('render_stock_id' in parameters) {
|
||||
render_stock_id = parameters['render_stock_id'];
|
||||
}
|
||||
|
||||
if (render_stock_id) {
|
||||
html += `<span class='float-right'><small>{% trans "Stock ID" %}: ${data.pk}</small></span>`;
|
||||
}
|
||||
|
||||
var render_location_detail = false;
|
||||
|
||||
if ('render_location_detail' in parameters) {
|
||||
render_location_detail = parameters['render_location_detail'];
|
||||
}
|
||||
|
||||
if (render_location_detail && data.location_detail) {
|
||||
html += `<span> - ${data.location_detail.name}</span>`;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -75,11 +112,17 @@ function renderStockLocation(name, data, parameters, options) {
|
||||
|
||||
var html = `<span>${level}${data.pathstring}</span>`;
|
||||
|
||||
if (data.description) {
|
||||
var render_description = true;
|
||||
|
||||
if ('render_description' in parameters) {
|
||||
render_description = parameters['render_description'];
|
||||
}
|
||||
|
||||
if (render_description && data.description) {
|
||||
html += ` - <i>${data.description}</i>`;
|
||||
}
|
||||
|
||||
html += `<span class='float-right'>{% trans "Location ID" %}: ${data.pk}</span>`;
|
||||
html += `<span class='float-right'><small>{% trans "Location ID" %}: ${data.pk}</small></span>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -96,7 +139,7 @@ function renderBuild(name, data, parameters, options) {
|
||||
var html = select2Thumbnail(image);
|
||||
|
||||
html += `<span><b>${data.reference}</b></span> - ${data.quantity} x ${data.part_detail.full_name}`;
|
||||
html += `<span class='float-right'>{% trans "Build ID" %}: ${data.pk}</span>`;
|
||||
html += `<span class='float-right'><small>{% trans "Build ID" %}: ${data.pk}</span></span>`;
|
||||
|
||||
html += `<p><i>${data.title}</i></p>`;
|
||||
|
||||
@ -116,7 +159,7 @@ function renderPart(name, data, parameters, options) {
|
||||
html += ` - <i>${data.description}</i>`;
|
||||
}
|
||||
|
||||
html += `<span class='float-right'>{% trans "Part ID" %}: ${data.pk}</span>`;
|
||||
html += `<span class='float-right'><small>{% trans "Part ID" %}: ${data.pk}</small></span>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -168,7 +211,7 @@ function renderPartCategory(name, data, parameters, options) {
|
||||
html += ` - <i>${data.description}</i>`;
|
||||
}
|
||||
|
||||
html += `<span class='float-right'>{% trans "Category ID" %}: ${data.pk}</span>`;
|
||||
html += `<span class='float-right'><small>{% trans "Category ID" %}: ${data.pk}</small></span>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -205,7 +248,7 @@ function renderManufacturerPart(name, data, parameters, options) {
|
||||
html += ` <span><b>${data.manufacturer_detail.name}</b> - ${data.MPN}</span>`;
|
||||
html += ` - <i>${data.part_detail.full_name}</i>`;
|
||||
|
||||
html += `<span class='float-right'>{% trans "Manufacturer Part ID" %}: ${data.pk}</span>`;
|
||||
html += `<span class='float-right'><small>{% trans "Manufacturer Part ID" %}: ${data.pk}</small></span>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
@ -234,7 +277,7 @@ function renderSupplierPart(name, data, parameters, options) {
|
||||
html += ` <span><b>${data.supplier_detail.name}</b> - ${data.SKU}</span>`;
|
||||
html += ` - <i>${data.part_detail.full_name}</i>`;
|
||||
|
||||
html += `<span class='float-right'>{% trans "Supplier Part ID" %}: ${data.pk}</span>`;
|
||||
html += `<span class='float-right'><small>{% trans "Supplier Part ID" %}: ${data.pk}</small></span>`;
|
||||
|
||||
|
||||
return html;
|
||||
|
@ -12,6 +12,7 @@
|
||||
loadTableFilters,
|
||||
makeIconBadge,
|
||||
purchaseOrderStatusDisplay,
|
||||
receivePurchaseOrderItems,
|
||||
renderLink,
|
||||
salesOrderStatusDisplay,
|
||||
setupFilterList,
|
||||
@ -234,6 +235,291 @@ function newPurchaseOrderFromOrderWizard(e) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Receive stock items against a PurchaseOrder
|
||||
* Uses the POReceive API endpoint
|
||||
*
|
||||
* arguments:
|
||||
* - order_id, ID / PK for the PurchaseOrder instance
|
||||
* - line_items: A list of PurchaseOrderLineItems objects to be allocated
|
||||
*
|
||||
* options:
|
||||
* -
|
||||
*/
|
||||
function receivePurchaseOrderItems(order_id, line_items, options={}) {
|
||||
|
||||
if (line_items.length == 0) {
|
||||
showAlertDialog(
|
||||
'{% trans "Select Line Items" %}',
|
||||
'{% trans "At least one line item must be selected" %}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
function renderLineItem(line_item, opts={}) {
|
||||
|
||||
var pk = line_item.pk;
|
||||
|
||||
// Part thumbnail + description
|
||||
var thumb = thumbnailImage(line_item.part_detail.thumbnail);
|
||||
|
||||
var quantity = (line_item.quantity || 0) - (line_item.received || 0);
|
||||
|
||||
if (quantity < 0) {
|
||||
quantity = 0;
|
||||
}
|
||||
|
||||
// Quantity to Receive
|
||||
var quantity_input = constructField(
|
||||
`items_quantity_${pk}`,
|
||||
{
|
||||
type: 'decimal',
|
||||
min_value: 0,
|
||||
value: quantity,
|
||||
title: '{% trans "Quantity to receive" %}',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Construct list of StockItem status codes
|
||||
var choices = [];
|
||||
|
||||
for (var key in stockCodes) {
|
||||
choices.push({
|
||||
value: key,
|
||||
display_name: stockCodes[key].value,
|
||||
});
|
||||
}
|
||||
|
||||
var destination_input = constructField(
|
||||
`items_location_${pk}`,
|
||||
{
|
||||
type: 'related field',
|
||||
label: '{% trans "Location" %}',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
var status_input = constructField(
|
||||
`items_status_${pk}`,
|
||||
{
|
||||
type: 'choice',
|
||||
label: '{% trans "Stock Status" %}',
|
||||
required: true,
|
||||
choices: choices,
|
||||
value: 10, // OK
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
|
||||
// Button to remove the row
|
||||
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 html = `
|
||||
<tr id='receive_row_${pk}' class='stock-receive-row'>
|
||||
<td id='part_${pk}'>
|
||||
${thumb} ${line_item.part_detail.full_name}
|
||||
</td>
|
||||
<td id='sku_${pk}'>
|
||||
${line_item.supplier_part_detail.SKU}
|
||||
</td>
|
||||
<td id='on_order_${pk}'>
|
||||
${line_item.quantity}
|
||||
</td>
|
||||
<td id='received_${pk}'>
|
||||
${line_item.received}
|
||||
</td>
|
||||
<td id='quantity_${pk}'>
|
||||
${quantity_input}
|
||||
</td>
|
||||
<td id='status_${pk}'>
|
||||
${status_input}
|
||||
</td>
|
||||
<td id='desination_${pk}'>
|
||||
${destination_input}
|
||||
</td>
|
||||
<td id='actions_${pk}'>
|
||||
${delete_button}
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
var table_entries = '';
|
||||
|
||||
line_items.forEach(function(item) {
|
||||
table_entries += renderLineItem(item);
|
||||
});
|
||||
|
||||
var html = ``;
|
||||
|
||||
// Add table
|
||||
html += `
|
||||
<table class='table table-striped table-condensed' id='order-receive-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Part" %}</th>
|
||||
<th>{% trans "Order Code" %}</th>
|
||||
<th>{% trans "Ordered" %}</th>
|
||||
<th>{% trans "Received" %}</th>
|
||||
<th style='min-width: 50px;'>{% trans "Receive" %}</th>
|
||||
<th style='min-width: 150px;'>{% trans "Status" %}</th>
|
||||
<th style='min-width: 300px;'>{% trans "Destination" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${table_entries}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
constructForm(`/api/order/po/${order_id}/receive/`, {
|
||||
method: 'POST',
|
||||
fields: {
|
||||
location: {},
|
||||
},
|
||||
preFormContent: html,
|
||||
confirm: true,
|
||||
confirmMessage: '{% trans "Confirm receipt of items" %}',
|
||||
title: '{% trans "Receive Purchase Order Items" %}',
|
||||
afterRender: function(fields, opts) {
|
||||
// Initialize the "destination" field for each item
|
||||
line_items.forEach(function(item) {
|
||||
|
||||
var pk = item.pk;
|
||||
|
||||
var name = `items_location_${pk}`;
|
||||
|
||||
var field_details = {
|
||||
name: name,
|
||||
api_url: '{% url "api-location-list" %}',
|
||||
filters: {
|
||||
|
||||
},
|
||||
type: 'related field',
|
||||
model: 'stocklocation',
|
||||
required: false,
|
||||
auto_fill: false,
|
||||
value: item.destination || item.part_detail.default_location,
|
||||
render_description: false,
|
||||
};
|
||||
|
||||
initializeRelatedField(
|
||||
field_details,
|
||||
null,
|
||||
opts,
|
||||
);
|
||||
|
||||
addClearCallback(
|
||||
name,
|
||||
field_details,
|
||||
opts
|
||||
);
|
||||
|
||||
initializeChoiceField(
|
||||
{
|
||||
name: `items_status_${pk}`,
|
||||
},
|
||||
null,
|
||||
opts
|
||||
);
|
||||
});
|
||||
|
||||
// Add callbacks to remove rows
|
||||
$(opts.modal).find('.button-row-remove').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
$(opts.modal).find(`#receive_row_${pk}`).remove();
|
||||
});
|
||||
},
|
||||
onSubmit: function(fields, opts) {
|
||||
// Extract data elements from the form
|
||||
var data = {
|
||||
items: [],
|
||||
location: getFormFieldValue('location', {}, opts),
|
||||
};
|
||||
|
||||
var item_pk_values = [];
|
||||
|
||||
line_items.forEach(function(item) {
|
||||
|
||||
var pk = item.pk;
|
||||
|
||||
var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts);
|
||||
|
||||
var status = getFormFieldValue(`items_status_${pk}`, {}, opts);
|
||||
|
||||
var location = getFormFieldValue(`items_location_${pk}`, {}, opts);
|
||||
|
||||
if (quantity != null) {
|
||||
data.items.push({
|
||||
line_item: pk,
|
||||
quantity: quantity,
|
||||
status: status,
|
||||
location: location,
|
||||
});
|
||||
|
||||
item_pk_values.push(pk);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Provide list of 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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function editPurchaseOrderLineItem(e) {
|
||||
|
||||
/* Edit a purchase order line item in a modal form.
|
||||
@ -280,12 +566,10 @@ function loadPurchaseOrderTable(table, options) {
|
||||
filters[key] = options.params[key];
|
||||
}
|
||||
|
||||
options.url = options.url || '{% url "api-po-list" %}';
|
||||
|
||||
setupFilterList('purchaseorder', $(table));
|
||||
|
||||
$(table).inventreeTable({
|
||||
url: options.url,
|
||||
url: '{% url "api-po-list" %}',
|
||||
queryParams: filters,
|
||||
name: 'purchaseorder',
|
||||
groupBy: false,
|
||||
@ -379,6 +663,21 @@ function loadPurchaseOrderTable(table, options) {
|
||||
*/
|
||||
function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
|
||||
options.params = options.params || {};
|
||||
|
||||
options.params['order'] = options.order;
|
||||
options.params['part_detail'] = true;
|
||||
|
||||
var filters = loadTableFilters('purchaseorderlineitem');
|
||||
|
||||
for (var key in options.params) {
|
||||
filters[key] = options.params[key];
|
||||
}
|
||||
|
||||
var target = options.filter_target || '#filter-list-purchase-order-lines';
|
||||
|
||||
setupFilterList('purchaseorderlineitem', $(table), target);
|
||||
|
||||
function setupCallbacks() {
|
||||
if (options.allow_edit) {
|
||||
$(table).find('.button-line-edit').click(function() {
|
||||
@ -424,22 +723,24 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
$(table).find('.button-line-receive').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
launchModalForm(`/order/purchase-order/${options.order}/receive/`, {
|
||||
success: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
},
|
||||
data: {
|
||||
line: pk,
|
||||
},
|
||||
secondary: [
|
||||
{
|
||||
field: 'location',
|
||||
label: '{% trans "New Location" %}',
|
||||
title: '{% trans "Create new stock location" %}',
|
||||
url: '{% url "stock-location-create" %}',
|
||||
},
|
||||
]
|
||||
});
|
||||
var line_item = $(table).bootstrapTable('getRowByUniqueId', pk);
|
||||
|
||||
if (!line_item) {
|
||||
console.log('WARNING: getRowByUniqueId returned null');
|
||||
return;
|
||||
}
|
||||
|
||||
receivePurchaseOrderItems(
|
||||
options.order,
|
||||
[
|
||||
line_item,
|
||||
],
|
||||
{
|
||||
success: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -451,17 +752,15 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
formatNoMatches: function() {
|
||||
return '{% trans "No line items found" %}';
|
||||
},
|
||||
queryParams: {
|
||||
order: options.order,
|
||||
part_detail: true
|
||||
},
|
||||
queryParams: filters,
|
||||
original: options.params,
|
||||
url: '{% url "api-po-line-list" %}',
|
||||
showFooter: true,
|
||||
uniqueId: 'pk',
|
||||
columns: [
|
||||
{
|
||||
field: 'pk',
|
||||
title: 'ID',
|
||||
visible: false,
|
||||
checkbox: true,
|
||||
visible: true,
|
||||
switchable: false,
|
||||
},
|
||||
{
|
||||
@ -618,7 +917,7 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
|
||||
}
|
||||
|
||||
if (options.allow_receive && row.received < row.quantity) {
|
||||
html += makeIconButton('fa-clipboard-check', 'button-line-receive', pk, '{% trans "Receive line item" %}');
|
||||
html += makeIconButton('fa-sign-in-alt', 'button-line-receive', pk, '{% trans "Receive line item" %}');
|
||||
}
|
||||
|
||||
html += `</div>`;
|
||||
|
@ -4,15 +4,12 @@
|
||||
|
||||
/* globals
|
||||
attachSelect,
|
||||
attachToggle,
|
||||
blankImage,
|
||||
enableField,
|
||||
clearField,
|
||||
clearFieldOptions,
|
||||
closeModal,
|
||||
constructField,
|
||||
constructFormBody,
|
||||
constructNumberInput,
|
||||
createNewModal,
|
||||
getFormFieldValue,
|
||||
global_settings,
|
||||
handleFormErrors,
|
||||
@ -247,7 +244,7 @@ function adjustStock(action, items, options={}) {
|
||||
break;
|
||||
}
|
||||
|
||||
var image = item.part_detail.thumbnail || item.part_detail.image || blankImage();
|
||||
var thumb = thumbnailImage(item.part_detail.thumbnail || item.part_detail.image);
|
||||
|
||||
var status = stockStatusDisplay(item.status, {
|
||||
classes: 'float-right'
|
||||
@ -268,14 +265,18 @@ function adjustStock(action, items, options={}) {
|
||||
var actionInput = '';
|
||||
|
||||
if (actionTitle != null) {
|
||||
actionInput = constructNumberInput(
|
||||
item.pk,
|
||||
actionInput = constructField(
|
||||
`items_quantity_${pk}`,
|
||||
{
|
||||
value: value,
|
||||
type: 'decimal',
|
||||
min_value: minValue,
|
||||
max_value: maxValue,
|
||||
read_only: readonly,
|
||||
value: value,
|
||||
title: readonly ? '{% trans "Quantity cannot be adjusted for serialized stock" %}' : '{% trans "Specify stock quantity" %}',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
hideLabels: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -293,7 +294,7 @@ function adjustStock(action, items, options={}) {
|
||||
|
||||
html += `
|
||||
<tr id='stock_item_${pk}' class='stock-item-row'>
|
||||
<td id='part_${pk}'><img src='${image}' class='hover-img-thumb'> ${item.part_detail.full_name}</td>
|
||||
<td id='part_${pk}'>${thumb} ${item.part_detail.full_name}</td>
|
||||
<td id='stock_${pk}'>${quantity}${status}</td>
|
||||
<td id='location_${pk}'>${location}</td>
|
||||
<td id='action_${pk}'>
|
||||
@ -319,50 +320,89 @@ function adjustStock(action, items, options={}) {
|
||||
|
||||
html += `</tbody></table>`;
|
||||
|
||||
var modal = createNewModal({
|
||||
title: formTitle,
|
||||
});
|
||||
var extraFields = {};
|
||||
|
||||
// Extra fields
|
||||
var extraFields = {
|
||||
location: {
|
||||
label: '{% trans "Location" %}',
|
||||
help_text: '{% trans "Select destination stock location" %}',
|
||||
type: 'related field',
|
||||
required: true,
|
||||
api_url: `/api/stock/location/`,
|
||||
model: 'stocklocation',
|
||||
name: 'location',
|
||||
},
|
||||
notes: {
|
||||
label: '{% trans "Notes" %}',
|
||||
help_text: '{% trans "Stock transaction notes" %}',
|
||||
type: 'string',
|
||||
name: 'notes',
|
||||
}
|
||||
};
|
||||
|
||||
if (!specifyLocation) {
|
||||
delete extraFields.location;
|
||||
if (specifyLocation) {
|
||||
extraFields.location = {};
|
||||
}
|
||||
|
||||
constructFormBody({}, {
|
||||
preFormContent: html,
|
||||
if (action != 'delete') {
|
||||
extraFields.notes = {};
|
||||
}
|
||||
|
||||
constructForm(url, {
|
||||
method: 'POST',
|
||||
fields: extraFields,
|
||||
preFormContent: html,
|
||||
confirm: true,
|
||||
confirmMessage: '{% trans "Confirm stock adjustment" %}',
|
||||
modal: modal,
|
||||
onSubmit: function(fields) {
|
||||
title: formTitle,
|
||||
afterRender: function(fields, opts) {
|
||||
// Add button callbacks to remove rows
|
||||
$(opts.modal).find('.button-stock-item-remove').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
// "Delete" action gets handled differently
|
||||
$(opts.modal).find(`#stock_item_${pk}`).remove();
|
||||
});
|
||||
|
||||
// Initialize "location" field
|
||||
if (specifyLocation) {
|
||||
initializeRelatedField(
|
||||
{
|
||||
name: 'location',
|
||||
type: 'related field',
|
||||
model: 'stocklocation',
|
||||
required: true,
|
||||
},
|
||||
null,
|
||||
opts
|
||||
);
|
||||
}
|
||||
},
|
||||
onSubmit: function(fields, opts) {
|
||||
|
||||
// Extract data elements from the form
|
||||
var data = {
|
||||
items: [],
|
||||
};
|
||||
|
||||
if (action != 'delete') {
|
||||
data.notes = getFormFieldValue('notes', {}, opts);
|
||||
}
|
||||
|
||||
if (specifyLocation) {
|
||||
data.location = getFormFieldValue('location', {}, opts);
|
||||
}
|
||||
|
||||
var item_pk_values = [];
|
||||
|
||||
items.forEach(function(item) {
|
||||
var pk = item.pk;
|
||||
|
||||
// Does the row exist in the form?
|
||||
var row = $(opts.modal).find(`#stock_item_${pk}`);
|
||||
|
||||
if (row) {
|
||||
|
||||
item_pk_values.push(pk);
|
||||
|
||||
var quantity = getFormFieldValue(`items_quantity_${pk}`, {}, opts);
|
||||
|
||||
data.items.push({
|
||||
pk: pk,
|
||||
quantity: quantity,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Delete action is handled differently
|
||||
if (action == 'delete') {
|
||||
|
||||
var requests = [];
|
||||
|
||||
items.forEach(function(item) {
|
||||
item_pk_values.forEach(function(pk) {
|
||||
requests.push(
|
||||
inventreeDelete(
|
||||
`/api/stock/${item.pk}/`,
|
||||
`/api/stock/${pk}/`,
|
||||
)
|
||||
);
|
||||
});
|
||||
@ -370,72 +410,40 @@ function adjustStock(action, items, options={}) {
|
||||
// Wait for *all* the requests to complete
|
||||
$.when.apply($, requests).done(function() {
|
||||
// Destroy the modal window
|
||||
$(modal).modal('hide');
|
||||
$(opts.modal).modal('hide');
|
||||
|
||||
if (options.onSuccess) {
|
||||
options.onSuccess();
|
||||
if (options.success) {
|
||||
options.success();
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Data to transmit
|
||||
var data = {
|
||||
items: [],
|
||||
opts.nested = {
|
||||
'items': item_pk_values,
|
||||
};
|
||||
|
||||
// Add values for each selected stock item
|
||||
items.forEach(function(item) {
|
||||
|
||||
var q = getFormFieldValue(item.pk, {}, {modal: modal});
|
||||
|
||||
if (q != null) {
|
||||
data.items.push({pk: item.pk, quantity: q});
|
||||
}
|
||||
});
|
||||
|
||||
// Add in extra field data
|
||||
for (var field_name in extraFields) {
|
||||
data[field_name] = getFormFieldValue(
|
||||
field_name,
|
||||
fields[field_name],
|
||||
{
|
||||
modal: modal,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
inventreePut(
|
||||
url,
|
||||
data,
|
||||
{
|
||||
method: 'POST',
|
||||
success: function() {
|
||||
success: function(response) {
|
||||
// Hide the modal
|
||||
$(opts.modal).modal('hide');
|
||||
|
||||
// Destroy the modal window
|
||||
$(modal).modal('hide');
|
||||
|
||||
if (options.onSuccess) {
|
||||
options.onSuccess();
|
||||
if (options.success) {
|
||||
options.success(response);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
switch (xhr.status) {
|
||||
case 400:
|
||||
|
||||
// Handle errors for standard fields
|
||||
handleFormErrors(
|
||||
xhr.responseJSON,
|
||||
extraFields,
|
||||
{
|
||||
modal: modal,
|
||||
}
|
||||
);
|
||||
|
||||
handleFormErrors(xhr.responseJSON, fields, opts);
|
||||
break;
|
||||
default:
|
||||
$(modal).modal('hide');
|
||||
$(opts.modal).modal('hide');
|
||||
showApiError(xhr);
|
||||
break;
|
||||
}
|
||||
@ -444,18 +452,6 @@ function adjustStock(action, items, options={}) {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Attach callbacks for the action buttons
|
||||
$(modal).find('.button-stock-item-remove').click(function() {
|
||||
var pk = $(this).attr('pk');
|
||||
|
||||
$(modal).find(`#stock_item_${pk}`).remove();
|
||||
});
|
||||
|
||||
attachToggle(modal);
|
||||
|
||||
$(modal + ' .select2-container').addClass('select-full-width');
|
||||
$(modal + ' .select2-container').css('width', '100%');
|
||||
}
|
||||
|
||||
|
||||
@ -1258,7 +1254,7 @@ function loadStockTable(table, options) {
|
||||
var items = $(table).bootstrapTable('getSelections');
|
||||
|
||||
adjustStock(action, items, {
|
||||
onSuccess: function() {
|
||||
success: function() {
|
||||
$(table).bootstrapTable('refresh');
|
||||
}
|
||||
});
|
||||
|
@ -274,7 +274,16 @@ function getAvailableTableFilters(tableKey) {
|
||||
};
|
||||
}
|
||||
|
||||
// Filters for the "Order" table
|
||||
// Filters for PurchaseOrderLineItem table
|
||||
if (tableKey == 'purchaseorderlineitem') {
|
||||
return {
|
||||
completed: {
|
||||
type: 'bool',
|
||||
title: '{% trans "Completed" %}',
|
||||
},
|
||||
};
|
||||
}
|
||||
// Filters for the PurchaseOrder table
|
||||
if (tableKey == 'purchaseorder') {
|
||||
|
||||
return {
|
||||
|
Reference in New Issue
Block a user