diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js
index 6360f396bb..23db95ccba 100644
--- a/InvenTree/templates/js/translated/stock.js
+++ b/InvenTree/templates/js/translated/stock.js
@@ -52,6 +52,7 @@
loadStockTestResultsTable,
loadStockTrackingTable,
loadTableFilters,
+ mergeStockItems,
removeStockRow,
serializeStockItem,
stockItemFields,
@@ -595,17 +596,17 @@ function assignStockToCustomer(items, options={}) {
buttons += '';
html += `
-
- ${thumbnail} ${part.full_name} |
-
-
- |
- ${location} |
- ${buttons} |
-
+
+ ${thumbnail} ${part.full_name} |
+
+
+ |
+ ${location} |
+ ${buttons} |
+
`;
}
@@ -615,13 +616,13 @@ function assignStockToCustomer(items, options={}) {
method: 'POST',
preFormContent: html,
fields: {
- 'customer': {
+ customer: {
value: options.customer,
filters: {
is_customer: true,
},
},
- 'notes': {},
+ notes: {},
},
confirm: true,
confirmMessage: '{% trans "Confirm stock assignment" %}',
@@ -694,6 +695,184 @@ function assignStockToCustomer(items, options={}) {
}
+/**
+ * Merge multiple stock items together
+ */
+function mergeStockItems(items, options={}) {
+
+ // Generate HTML content for the form
+ var html = `
+
+
{% trans "Warning: Merge operation cannot be reversed" %}
+
{% trans "Some information will be lost when merging stock items" %}:
+
+ - {% trans "Stock transaction history will be deleted for merged items" %}
+ - {% trans "Supplier part information will be deleted for merged items" %}
+
+
+ `;
+
+ html += `
+
+
+
+ {% trans "Part" %} |
+ {% trans "Stock Item" %} |
+ {% trans "Location" %} |
+ |
+
+
+
+ `;
+
+ // Keep track of how many "locations" there are
+ var locations = [];
+
+ for (var idx = 0; idx < items.length; idx++) {
+ var item = items[idx];
+
+ var pk = item.pk;
+
+ if (item.location && !locations.includes(item.location)) {
+ locations.push(item.location);
+ }
+
+ var part = item.part_detail;
+ var location = locationDetail(item, false);
+
+ var thumbnail = thumbnailImage(part.thumbnail || part.image);
+
+ var quantity = '';
+
+ if (item.serial && item.quantity == 1) {
+ quantity = `{% trans "Serial" %}: ${item.serial}`;
+ } else {
+ quantity = `{% trans "Quantity" %}: ${item.quantity}`;
+ }
+
+ quantity += stockStatusDisplay(item.status, {classes: 'float-right'});
+
+ var buttons = ``;
+
+ buttons += makeIconButton(
+ 'fa-times icon-red',
+ 'button-stock-item-remove',
+ pk,
+ '{% trans "Remove row" %}',
+ );
+
+ html += `
+
+ ${thumbnail} ${part.full_name} |
+
+
+ |
+ ${location} |
+ ${buttons} |
+
+ `;
+ }
+
+ html += '
';
+
+ var location = locations.length == 1 ? locations[0] : null;
+
+ constructForm('{% url "api-stock-merge" %}', {
+ method: 'POST',
+ preFormContent: html,
+ fields: {
+ location: {
+ value: location,
+ icon: 'fa-sitemap',
+ },
+ notes: {},
+ allow_mismatched_suppliers: {},
+ allow_mismatched_status: {},
+ },
+ confirm: true,
+ confirmMessage: '{% trans "Confirm stock item merge" %}',
+ title: '{% trans "Merge Stock Items" %}',
+ afterRender: function(fields, opts) {
+ // Add button callbacks to remove rows
+ $(opts.modal).find('.button-stock-item-remove').click(function() {
+ var pk = $(this).attr('pk');
+
+ $(opts.modal).find(`#stock_item_${pk}`).remove();
+ });
+ },
+ onSubmit: function(fields, opts) {
+
+ // Extract data elements from the form
+ var data = {
+ items: [],
+ };
+
+ var item_pk_values = [];
+
+ items.forEach(function(item) {
+ var pk = item.pk;
+
+ // Does the row still exist in the form?
+ var row = $(opts.modal).find(`#stock_item_${pk}`);
+
+ if (row.exists()) {
+ item_pk_values.push(pk);
+
+ data.items.push({
+ item: pk,
+ });
+ }
+ });
+
+ var extra_fields = [
+ 'location',
+ 'notes',
+ 'allow_mismatched_suppliers',
+ 'allow_mismatched_status',
+ ];
+
+ extra_fields.forEach(function(field) {
+ data[field] = getFormFieldValue(field, fields[field], opts);
+ });
+
+ opts.nested = {
+ 'items': item_pk_values
+ };
+
+ // Submit the form data
+ inventreePut(
+ '{% url "api-stock-merge" %}',
+ 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;
+ }
+ }
+ }
+ )
+ }
+ });
+}
+
+
/**
* Perform stock adjustments
*/
@@ -1875,6 +2054,20 @@ function loadStockTable(table, options) {
stockAdjustment('move');
});
+ $('#multi-item-merge').click(function() {
+ var items = $(table).bootstrapTable('getSelections');
+
+ mergeStockItems(items, {
+ success: function(response) {
+ $(table).bootstrapTable('refresh');
+
+ showMessage('{% trans "Merged stock items" %}', {
+ style: 'success',
+ });
+ }
+ });
+ });
+
$('#multi-item-assign').click(function() {
var items = $(table).bootstrapTable('getSelections');
diff --git a/InvenTree/templates/stock_table.html b/InvenTree/templates/stock_table.html
index 1f873d7c58..4a20938869 100644
--- a/InvenTree/templates/stock_table.html
+++ b/InvenTree/templates/stock_table.html
@@ -49,6 +49,7 @@
{% trans "Remove stock" %}
{% trans "Count stock" %}
{% trans "Move stock" %}
+ {% trans "Merge stock" %}
{% trans "Order stock" %}
{% trans "Assign to customer" %}
{% trans "Change stock status" %}