2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-09 21:30:54 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into plugin-2037

This commit is contained in:
Matthias
2021-10-08 22:21:11 +02:00
74 changed files with 3305 additions and 2378 deletions

View File

@@ -43,6 +43,12 @@
</a>
</li>
<li class='list-group-item' title='{% trans "Forms" %}'>
<a href='#' class='nav-toggle' id='select-user-forms'>
<span class='fas fa-table'></span>{% trans "Forms" %}
</a>
</li>
<!--
<li class='list-group-item' title='{% trans "Settings" %}'>
<a href='#' class='nav-toggle' id='select-user-settings'>

View File

@@ -17,8 +17,8 @@
{% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %}
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_DUPLICATE_IPN" %}
{% include "InvenTree/settings/setting.html" with key="PART_ALLOW_EDIT_IPN" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_FORMS" icon="fa-dollar-sign" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_PRICE_IN_BOM" icon="fa-dollar-sign" %}
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_RELATED" icon="fa-random" %}
{% include "InvenTree/settings/setting.html" with key="PART_CREATE_INITIAL" icon="fa-boxes" %}
<tr><td colspan='5'></td></tr>

View File

@@ -21,6 +21,7 @@
{% include "InvenTree/settings/user_search.html" %}
{% include "InvenTree/settings/user_labels.html" %}
{% include "InvenTree/settings/user_reports.html" %}
{% include "InvenTree/settings/user_forms.html" %}
{% if user.is_staff %}

View File

@@ -0,0 +1,22 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}user-forms{% endblock %}
{% block heading %}
{% trans "Form Settings" %}
{% endblock %}
{% block content %}
<div class='row'>
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="PART_SHOW_QUANTITY_IN_FORMS" icon="fa-hashtag" user_setting=True %}
</tbody>
</table>
</div>
{% endblock %}

View File

@@ -10,6 +10,7 @@
/* exported
attachClipboard,
enableDragAndDrop,
exportFormatOptions,
inventreeDocReady,
inventreeLoad,
inventreeSave,
@@ -46,6 +47,31 @@ function attachClipboard(selector, containerselector, textElement) {
}
/**
* Return a standard list of export format options *
*/
function exportFormatOptions() {
return [
{
value: 'csv',
display_name: 'CSV',
},
{
value: 'tsv',
display_name: 'TSV',
},
{
value: 'xls',
display_name: 'XLS',
},
{
value: 'xlsx',
display_name: 'XLSX',
},
];
}
function inventreeDocReady() {
/* Run this function when the HTML document is loaded.
* This will be called for every page that extends "base.html"

View File

@@ -148,6 +148,13 @@ function loadBomTable(table, options) {
ordering: 'name',
};
// Do we show part pricing in the BOM table?
var show_pricing = global_settings.PART_SHOW_PRICE_IN_BOM;
if (!show_pricing) {
params.include_pricing = false;
}
if (options.part_detail) {
params.part_detail = true;
}
@@ -282,32 +289,34 @@ function loadBomTable(table, options) {
}
});
cols.push({
field: 'purchase_price_range',
title: '{% trans "Purchase Price Range" %}',
searchable: false,
sortable: true,
});
if (show_pricing) {
cols.push({
field: 'purchase_price_range',
title: '{% trans "Purchase Price Range" %}',
searchable: false,
sortable: true,
});
cols.push({
field: 'purchase_price_avg',
title: '{% trans "Purchase Price Average" %}',
searchable: false,
sortable: true,
});
cols.push({
field: 'purchase_price_avg',
title: '{% trans "Purchase Price Average" %}',
searchable: false,
sortable: true,
});
cols.push({
field: 'price_range',
title: '{% trans "Supplier Cost" %}',
sortable: true,
formatter: function(value) {
if (value) {
return value;
} else {
return `<span class='warning-msg'>{% trans 'No supplier pricing available' %}</span>`;
cols.push({
field: 'price_range',
title: '{% trans "Supplier Cost" %}',
sortable: true,
formatter: function(value) {
if (value) {
return value;
} else {
return `<span class='warning-msg'>{% trans 'No supplier pricing available' %}</span>`;
}
}
}
});
});
}
cols.push({
field: 'optional',

View File

@@ -4,7 +4,6 @@
/* globals
buildStatusDisplay,
constructForm,
getFieldByName,
global_settings,
imageHoverIcon,
inventreeGet,
@@ -20,6 +19,7 @@
*/
/* exported
allocateStockToBuild,
editBuildOrder,
loadAllocationTable,
loadBuildOrderAllocationTable,
@@ -42,6 +42,8 @@ function buildFormFields() {
part_detail: true,
}
},
sales_order: {
},
batch: {},
target_date: {},
take_from: {},
@@ -76,23 +78,32 @@ function newBuildOrder(options={}) {
var fields = buildFormFields();
// Specify the target part
if (options.part) {
fields.part.value = options.part;
}
// Specify the desired quantity
if (options.quantity) {
fields.quantity.value = options.quantity;
}
// Specify the parent build order
if (options.parent) {
fields.parent.value = options.parent;
}
// Specify a parent sales order
if (options.sales_order) {
fields.sales_order.value = options.sales_order;
}
constructForm(`/api/build/`, {
fields: fields,
follow: true,
method: 'POST',
title: '{% trans "Create Build Order" %}'
title: '{% trans "Create Build Order" %}',
onSuccess: options.onSuccess,
});
}
@@ -102,6 +113,7 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
*/
var buildId = buildInfo.pk;
var partId = buildInfo.part;
var outputId = 'untracked';
@@ -120,11 +132,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 +147,6 @@ function makeBuildOutputActionButtons(output, buildInfo, lines) {
);
}
if (output) {
// Add a button to "complete" the particular build output
@@ -163,11 +173,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 +360,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 +389,8 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
quantity += item.quantity;
});
row.allocated = quantity;
return quantity;
}
@@ -377,52 +403,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 +625,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 +687,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 +833,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

View File

@@ -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();

View File

@@ -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%',
});
}
@@ -1422,6 +1568,9 @@ function renderModelData(name, model, data, parameters, options) {
case 'partparametertemplate':
renderer = renderPartParameterTemplate;
break;
case 'salesorder':
renderer = renderSalesOrder;
break;
case 'manufacturerpart':
renderer = renderManufacturerPart;
break;
@@ -1884,7 +2033,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

View File

@@ -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

View File

@@ -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,24 @@ function renderPart(name, data, parameters, options) {
html += ` - <i>${data.description}</i>`;
}
html += `<span class='float-right'>{% trans "Part ID" %}: ${data.pk}</span>`;
var stock = '';
// Display available part quantity
if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) {
if (data.in_stock == 0) {
stock = `<span class='label-form label-red'>{% trans "No Stock" %}</span>`;
} else {
stock = `<span class='label-form label-green'>{% trans "In Stock" %}: ${data.in_stock}</span>`;
}
}
html += `
<span class='float-right'>
<small>
${stock}
{% trans "Part ID" %}: ${data.pk}
</small>
</span>`;
return html;
}
@@ -156,6 +216,26 @@ function renderOwner(name, data, parameters, options) {
}
// Renderer for "SalesOrder" model
// eslint-disable-next-line no-unused-vars
function renderSalesOrder(name, data, parameters, options) {
var html = `<span>${data.reference}</span>`;
if (data.description) {
html += ` - <i>${data.description}</i>`;
}
html += `
<span class='float-right'>
<small>
{% trans "Order ID" %}: ${data.pk}
</small>
</span>`;
return html;
}
// Renderer for "PartCategory" model
// eslint-disable-next-line no-unused-vars
function renderPartCategory(name, data, parameters, options) {
@@ -168,7 +248,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 +285,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 +314,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;

File diff suppressed because it is too large Load Diff

View File

@@ -4,15 +4,12 @@
/* globals
attachSelect,
attachToggle,
blankImage,
enableField,
clearField,
clearFieldOptions,
closeModal,
constructField,
constructFormBody,
constructNumberInput,
createNewModal,
getFormFieldValue,
global_settings,
handleFormErrors,
@@ -101,24 +98,7 @@ function exportStock(params={}) {
required: true,
type: 'choice',
value: 'csv',
choices: [
{
value: 'csv',
display_name: 'CSV',
},
{
value: 'tsv',
display_name: 'TSV',
},
{
value: 'xls',
display_name: 'XLS',
},
{
value: 'xlsx',
display_name: 'XLSX',
},
],
choices: exportFormatOptions(),
},
sublocations: {
label: '{% trans "Include Sublocations" %}',
@@ -247,7 +227,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 +248,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 +277,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 +303,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 +393,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 +435,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 +1237,7 @@ function loadStockTable(table, options) {
var items = $(table).bootstrapTable('getSelections');
adjustStock(action, items, {
onSuccess: function() {
success: function() {
$(table).bootstrapTable('refresh');
}
});

View File

@@ -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 {