From 0507e8a3bc9805231c391b2430cb177fac82df4c Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 10 Jul 2021 23:59:35 +1000 Subject: [PATCH 01/26] Building stock adjustment modal --- InvenTree/templates/js/stock.js | 94 +++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 524b116743..efa3c30ada 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -750,6 +750,100 @@ function loadStockTable(table, options) { stock.push(item.pk); }); + var title = 'Form title'; + + switch (action) { + case 'move': + title = '{% trans "Transfer Stock" %}'; + break; + case 'count': + title = '{% trans "Count Stock" %}'; + break; + case 'take': + title = '{% trans "Remove Stock" %}'; + break; + case 'add': + title = '{% trans "Add Stock" %}'; + break; + case 'delete': + title = '{% trans "Delete Stock" %}'; + break; + default: + break; + } + + var modal = createNewModal({ + title: title, + }); + + // Generate content for the modal + + var html = ` + + + + + + + + + + + `; + + items.forEach(function(item) { + + var pk = item.pk; + + var image = item.part_detail.thumbnail || item.part_detail.image || blankImage(); + + var status = stockStatusDisplay(item.status, { + classes: 'float-right' + }); + + var quantity = item.quantity; + + if (item.serial != null) { + quantity = `#${item.serial}`; + } + + var buttons = `
`; + + buttons += makeIconButton( + 'fa-trash-alt icon-red', + 'button-stock-item-remove', + pk, + '{% trans "Remove stock item" %}', + ); + + buttons += `
`; + + html += ` + + + + + + `; + + }); + + html += `
{% trans "Part" %}{% trans "Stock" %}{% trans "Location" %}
${item.part_detail.full_name}${status}${quantity}${item.location_detail.pathstring}${buttons}
`; + + $(modal).find('.modal-form-content').html(html); + + // Add a "confirm" button + insertConfirmButton({ + modal: modal, + }); + + attachToggle(modal); + + $(modal + ' .select2-container').addClass('select-full-width'); + $(modal + ' .select2-container').css('width', '100%'); + + return; + // Buttons for launching secondary modals var secondary = []; From 14ab1bef14370b330215cb15cc87a6ac499b3041 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jul 2021 00:15:46 +1000 Subject: [PATCH 02/26] Callback to remove row --- InvenTree/templates/js/stock.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index efa3c30ada..2413d36089 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -832,6 +832,13 @@ function loadStockTable(table, options) { $(modal).find('.modal-form-content').html(html); + // 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(); + }); + // Add a "confirm" button insertConfirmButton({ modal: modal, From c045a3b6f67f05200df674e1a2920127ed781cef Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jul 2021 20:36:52 +1000 Subject: [PATCH 03/26] Refactorin' --- InvenTree/stock/api.py | 13 +- InvenTree/templates/js/forms.js | 12 +- InvenTree/templates/js/stock.js | 340 ++++++++++++++++++-------------- 3 files changed, 205 insertions(+), 160 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 4a6e7111e8..64be7c885f 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -143,7 +143,7 @@ class StockAdjust(APIView): elif 'items' in request.data: _items = request.data['items'] else: - raise ValidationError({'items': 'Request must contain list of stock items'}) + raise ValidationError({'items': _('Request must contain list of stock items')}) # List of validated items self.items = [] @@ -151,13 +151,14 @@ class StockAdjust(APIView): for entry in _items: if not type(entry) == dict: - raise ValidationError({'error': 'Improperly formatted data'}) + raise ValidationError({'error': _('Improperly formatted data')}) try: - pk = entry.get('pk', None) + # Look for 'pk' value first, with 'id' as a backup + pk = entry.get('pk', entry.get('id', None)) item = StockItem.objects.get(pk=pk) except (ValueError, StockItem.DoesNotExist): - raise ValidationError({'pk': 'Each entry must contain a valid pk field'}) + raise ValidationError({'pk': _('Each entry must contain a valid pk field')}) if self.allow_missing_quantity and 'quantity' not in entry: entry['quantity'] = item.quantity @@ -165,10 +166,10 @@ class StockAdjust(APIView): try: quantity = Decimal(str(entry.get('quantity', None))) except (ValueError, TypeError, InvalidOperation): - raise ValidationError({'quantity': "Each entry must contain a valid quantity value"}) + raise ValidationError({'quantity': _("Each entry must contain a valid quantity value")}) if quantity < 0: - raise ValidationError({'quantity': 'Quantity field must not be less than zero'}) + raise ValidationError({'quantity': _('Quantity field must not be less than zero')}) self.items.append({ 'item': item, diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index b7af665393..ffe3868746 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -441,7 +441,17 @@ function constructFormBody(fields, options) { modalEnable(modal, true); // Insert generated form content - $(modal).find('.modal-form-content').html(html); + $(modal).find('#form-content').html(html); + + if (options.preFormContent) { + console.log('pre form content', options.preFormContent); + $(modal).find('#pre-form-content').html(options.preFormContent); + } + + if (options.postFormContent) { + console.log('post form content', options.postFormContent); + $(modal).find('#post-form-content').html(options.postFormContent); + } // Clear any existing buttons from the modal $(modal).find('#modal-footer-buttons').html(''); diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 2413d36089..42cbc74a68 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -20,6 +20,138 @@ function stockStatusCodes() { } +/** + * Perform stock adjustments + */ +function adjustStock(items, options={}) { + + var formTitle = 'Form Title Here'; + var actionTitle = null; + + switch (options.action) { + case 'move': + formTitle = '{% trans "Transfer Stock" %}'; + actionTitle = '{% trans "Move" %}'; + break; + case 'count': + formTitle = '{% trans "Count Stock" %}'; + actionTitle = '{% trans "Count" %}'; + break; + case 'take': + formTitle = '{% trans "Remove Stock" %}'; + actionTitle = '{% trans "Take" %}'; + break; + case 'add': + formTitle = '{% trans "Add Stock" %}'; + actionTitle = '{% trans "Add" %}'; + break; + case 'delete': + formTitle = '{% trans "Delete Stock" %}'; + break; + default: + break; + } + + // Generate modal HTML content + var html = ` + + + + + + + + + + + `; + + items.forEach(function(item) { + + var pk = item.pk; + + var image = item.part_detail.thumbnail || item.part_detail.image || blankImage(); + + var status = stockStatusDisplay(item.status, { + classes: 'float-right' + }); + + var quantity = item.quantity; + + var location = locationDetail(item, false); + + if (item.location_detail) { + location = item.location_detail.pathstring; + } + + if (item.serial != null) { + quantity = `#${item.serial}`; + } + + var actionInput = ''; + + if (actionTitle != null) { + actionInput = constructNumberInput( + item.pk, + { + value: item.quantity, + min_value: 0, + } + ) + }; + + var buttons = `
`; + + buttons += makeIconButton( + 'fa-trash-alt icon-red', + 'button-stock-item-remove', + pk, + '{% trans "Remove stock item" %}', + ); + + buttons += `
`; + + html += ` + + + + + + `; + + }); + + html += `
{% trans "Part" %}{% trans "Stock" %}{% trans "Location" %}${actionTitle || ''}
${item.part_detail.full_name}${quantity}${status}${location} +
+ ${actionInput} + ${buttons} +
+
`; + + var modal = createNewModal({ + title: formTitle, + }); + + constructFormBody({}, { + preFormContent: html, + confirm: true, + modal: modal, + }); + + // 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%'); +} + + function removeStockRow(e) { // Remove a selected row from a stock modal form @@ -228,6 +360,58 @@ function loadStockTestResultsTable(table, options) { } + +function locationDetail(row, showLink=true) { + /* + * Function to display a "location" of a StockItem. + * + * Complicating factors: A StockItem may not actually *be* in a location! + * - Could be at a customer + * - Could be installed in another stock item + * - Could be assigned to a sales order + * - Could be currently in production! + * + * So, instead of being naive, we'll check! + */ + + // Display text + var text = ''; + + // URL (optional) + var url = ''; + + if (row.is_building && row.build) { + // StockItem is currently being built! + text = '{% trans "In production" %}'; + url = `/build/${row.build}/`; + } else if (row.belongs_to) { + // StockItem is installed inside a different StockItem + text = `{% trans "Installed in Stock Item" %} ${row.belongs_to}`; + url = `/stock/item/${row.belongs_to}/installed/`; + } else if (row.customer) { + // StockItem has been assigned to a customer + text = '{% trans "Shipped to customer" %}'; + url = `/company/${row.customer}/assigned-stock/`; + } else if (row.sales_order) { + // StockItem has been assigned to a sales order + text = '{% trans "Assigned to Sales Order" %}'; + url = `/order/sales-order/${row.sales_order}/`; + } else if (row.location) { + text = row.location_detail.pathstring; + url = `/stock/location/${row.location}/`; + } else { + text = '{% trans "No stock location set" %}'; + url = ''; + } + + if (showLink && url) { + return renderLink(text, url); + } else { + return text; + } +} + + function loadStockTable(table, options) { /* Load data into a stock table with adjustable options. * Fetches data (via AJAX) and loads into a bootstrap table. @@ -271,56 +455,6 @@ function loadStockTable(table, options) { filters[key] = params[key]; } - function locationDetail(row) { - /* - * Function to display a "location" of a StockItem. - * - * Complicating factors: A StockItem may not actually *be* in a location! - * - Could be at a customer - * - Could be installed in another stock item - * - Could be assigned to a sales order - * - Could be currently in production! - * - * So, instead of being naive, we'll check! - */ - - // Display text - var text = ''; - - // URL (optional) - var url = ''; - - if (row.is_building && row.build) { - // StockItem is currently being built! - text = '{% trans "In production" %}'; - url = `/build/${row.build}/`; - } else if (row.belongs_to) { - // StockItem is installed inside a different StockItem - text = `{% trans "Installed in Stock Item" %} ${row.belongs_to}`; - url = `/stock/item/${row.belongs_to}/installed/`; - } else if (row.customer) { - // StockItem has been assigned to a customer - text = '{% trans "Shipped to customer" %}'; - url = `/company/${row.customer}/assigned-stock/`; - } else if (row.sales_order) { - // StockItem has been assigned to a sales order - text = '{% trans "Assigned to Sales Order" %}'; - url = `/order/sales-order/${row.sales_order}/`; - } else if (row.location) { - text = row.location_detail.pathstring; - url = `/stock/location/${row.location}/`; - } else { - text = '{% trans "No stock location set" %}'; - url = ''; - } - - if (url) { - return renderLink(text, url); - } else { - return text; - } - } - var grouping = true; if ('grouping' in options) { @@ -741,114 +875,14 @@ function loadStockTable(table, options) { ] ); + function stockAdjustment(action) { var items = $("#stock-table").bootstrapTable("getSelections"); - var stock = []; - - items.forEach(function(item) { - stock.push(item.pk); + adjustStock(items, { + action: action, }); - var title = 'Form title'; - - switch (action) { - case 'move': - title = '{% trans "Transfer Stock" %}'; - break; - case 'count': - title = '{% trans "Count Stock" %}'; - break; - case 'take': - title = '{% trans "Remove Stock" %}'; - break; - case 'add': - title = '{% trans "Add Stock" %}'; - break; - case 'delete': - title = '{% trans "Delete Stock" %}'; - break; - default: - break; - } - - var modal = createNewModal({ - title: title, - }); - - // Generate content for the modal - - var html = ` - - - - - - - - - - - `; - - items.forEach(function(item) { - - var pk = item.pk; - - var image = item.part_detail.thumbnail || item.part_detail.image || blankImage(); - - var status = stockStatusDisplay(item.status, { - classes: 'float-right' - }); - - var quantity = item.quantity; - - if (item.serial != null) { - quantity = `#${item.serial}`; - } - - var buttons = `
`; - - buttons += makeIconButton( - 'fa-trash-alt icon-red', - 'button-stock-item-remove', - pk, - '{% trans "Remove stock item" %}', - ); - - buttons += `
`; - - html += ` - - - - - - `; - - }); - - html += `
{% trans "Part" %}{% trans "Stock" %}{% trans "Location" %}
${item.part_detail.full_name}${status}${quantity}${item.location_detail.pathstring}${buttons}
`; - - $(modal).find('.modal-form-content').html(html); - - // 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(); - }); - - // Add a "confirm" button - insertConfirmButton({ - modal: modal, - }); - - attachToggle(modal); - - $(modal + ' .select2-container').addClass('select-full-width'); - $(modal + ' .select2-container').css('width', '100%'); - return; // Buttons for launching secondary modals From 9e4bc274cff3e13c8bad3cb64300ba3174b8d369 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jul 2021 20:40:27 +1000 Subject: [PATCH 04/26] Allow custom code to be run on form submission --- InvenTree/templates/js/forms.js | 22 +++++++++++++++------- InvenTree/templates/js/stock.js | 3 +++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index ffe3868746..722db85d1c 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -444,12 +444,10 @@ function constructFormBody(fields, options) { $(modal).find('#form-content').html(html); if (options.preFormContent) { - console.log('pre form content', options.preFormContent); $(modal).find('#pre-form-content').html(options.preFormContent); } if (options.postFormContent) { - console.log('post form content', options.postFormContent); $(modal).find('#post-form-content').html(options.postFormContent); } @@ -484,7 +482,21 @@ function constructFormBody(fields, options) { $(modal).on('click', '#modal-form-submit', function() { - submitFormData(fields, options); + // Immediately disable the "submit" button, + // to prevent the form being submitted multiple times! + $(options.modal).find('#modal-form-submit').prop('disabled', true); + + // Run custom code before normal form submission + if (options.beforeSubmit) { + options.beforeSubmit(fields, options); + } + + // Run custom code instead of normal form submission + if (options.onSubmit) { + options.onSubmit(fields, options); + } else { + submitFormData(fields, options); + } }); } @@ -521,10 +533,6 @@ function insertConfirmButton(options) { */ function submitFormData(fields, options) { - // Immediately disable the "submit" button, - // to prevent the form being submitted multiple times! - $(options.modal).find('#modal-form-submit').prop('disabled', true); - // Form data to be uploaded to the server // Only used if file / image upload is required var form_data = new FormData(); diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 42cbc74a68..339b0c4f43 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -136,6 +136,9 @@ function adjustStock(items, options={}) { preFormContent: html, confirm: true, modal: modal, + onSubmit: function(fields, options) { + console.log("submit!"); + } }); // Attach callbacks for the action buttons From ca5d3a57de0804997634852ca8d74b4b6307dc4a Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jul 2021 20:49:54 +1000 Subject: [PATCH 05/26] Set quantity input parameters based on action --- InvenTree/templates/js/forms.js | 10 +++++----- InvenTree/templates/js/stock.js | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 722db85d1c..f0807bda28 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -1482,21 +1482,21 @@ function constructInputOptions(name, classes, type, parameters) { opts.push(`readonly=''`); } - if (parameters.value) { + if (parameters.value != null) { // Existing value? opts.push(`value='${parameters.value}'`); - } else if (parameters.default) { + } else if (parameters.default != null) { // Otherwise, a defualt value? opts.push(`value='${parameters.default}'`); } // Maximum input length - if (parameters.max_length) { + if (parameters.max_length != null) { opts.push(`maxlength='${parameters.max_length}'`); } // Minimum input length - if (parameters.min_length) { + if (parameters.min_length != null) { opts.push(`minlength='${parameters.min_length}'`); } @@ -1516,7 +1516,7 @@ function constructInputOptions(name, classes, type, parameters) { } // Placeholder? - if (parameters.placeholder) { + if (parameters.placeholder != null) { opts.push(`placeholder='${parameters.placeholder}'`); } diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 339b0c4f43..a2be9b4fc0 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -70,6 +70,33 @@ function adjustStock(items, options={}) { var pk = item.pk; + var readonly = (item.serial != null); + var minValue = null; + var maxValue = null; + var value = null; + + switch (options.action) { + case 'move': + minValue = 0; + maxValue = item.quantity; + value = item.quantity; + break; + case 'add': + minValue = 0; + value = 0; + break; + case 'take': + minValue = 0; + value = 0; + break; + case 'count': + minValue = 0; + value = item.quantity; + break; + default: + break; + } + var image = item.part_detail.thumbnail || item.part_detail.image || blankImage(); var status = stockStatusDisplay(item.status, { @@ -94,8 +121,10 @@ function adjustStock(items, options={}) { actionInput = constructNumberInput( item.pk, { - value: item.quantity, - min_value: 0, + value: value, + min_value: minValue, + max_value: maxValue, + readonly: readonly, } ) }; From 3efd7f7777362a3d3d8e42eb3e0e42880a364d9c Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jul 2021 20:56:29 +1000 Subject: [PATCH 06/26] Add a "notes" field --- InvenTree/templates/js/forms.js | 9 +++++---- InvenTree/templates/js/stock.js | 7 +++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index f0807bda28..d68ce64c45 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -395,10 +395,11 @@ function constructFormBody(fields, options) { for (var name in displayed_fields) { - // Only push names which are actually in the set of fields - if (name in fields) { - field_names.push(name); - } else { + field_names.push(name); + + // Field not specified in the API, but the client wishes to add it! + if (!(name in fields)) { + fields[name] = displayed_fields[name]; console.log(`WARNING: '${name}' does not match a valid field name.`); } } diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index a2be9b4fc0..e78e06c0ff 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -162,6 +162,13 @@ function adjustStock(items, options={}) { }); constructFormBody({}, { + fields: { + note: { + label: '{% trans "Notes" %}', + help_text: '{% trans "Stock transaction notes" %}', + type: 'string', + } + }, preFormContent: html, confirm: true, modal: modal, From 9eb1367d8047a9163f9f722d6c462976eecdbf5a Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jul 2021 21:07:56 +1000 Subject: [PATCH 07/26] Add "location" field --- InvenTree/templates/js/forms.js | 1 - InvenTree/templates/js/stock.js | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index d68ce64c45..490b67944f 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -400,7 +400,6 @@ function constructFormBody(fields, options) { // Field not specified in the API, but the client wishes to add it! if (!(name in fields)) { fields[name] = displayed_fields[name]; - console.log(`WARNING: '${name}' does not match a valid field name.`); } } diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index e78e06c0ff..80249b2870 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -132,7 +132,7 @@ function adjustStock(items, options={}) { var buttons = `
`; buttons += makeIconButton( - 'fa-trash-alt icon-red', + 'fa-times icon-red', 'button-stock-item-remove', pk, '{% trans "Remove stock item" %}', @@ -163,6 +163,14 @@ function adjustStock(items, options={}) { constructFormBody({}, { fields: { + location: { + label: '{% trans "Location" %}', + help_text: '{% trans "Select stock location" %}', + type: 'related field', + required: true, + api_url: `/api/stock/location/`, + model: 'stocklocation', + }, note: { label: '{% trans "Notes" %}', help_text: '{% trans "Stock transaction notes" %}', From cc90c8abbe55dad99db55bb6a662fa7a0cd5c4e7 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jul 2021 21:15:06 +1000 Subject: [PATCH 08/26] Move buttons to separate table column --- InvenTree/templates/js/stock.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 80249b2870..108a5e595c 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -61,6 +61,7 @@ function adjustStock(items, options={}) { {% trans "Stock" %} {% trans "Location" %} ${actionTitle || ''} + @@ -145,12 +146,8 @@ function adjustStock(items, options={}) { ${item.part_detail.full_name} ${quantity}${status} ${location} - -
- ${actionInput} - ${buttons} -
- + ${actionInput} + ${buttons} `; }); From 7531984c788f625d87de669b99db92f0fc36fef3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 11 Jul 2021 21:17:54 +1000 Subject: [PATCH 09/26] Fix read_only attribute --- InvenTree/templates/js/stock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 108a5e595c..7fa58a099f 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -125,7 +125,7 @@ function adjustStock(items, options={}) { value: value, min_value: minValue, max_value: maxValue, - readonly: readonly, + read_only: readonly, } ) }; From 747cccfa42bffcf54b58c8297293e31a8784aa50 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 16:55:35 +1000 Subject: [PATCH 10/26] Refactor to use more generic forms approach --- InvenTree/templates/js/stock.js | 66 ++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 7fa58a099f..50bc49947f 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -28,10 +28,15 @@ function adjustStock(items, options={}) { var formTitle = 'Form Title Here'; var actionTitle = null; + var specifyLocation = false; + var allowSerializedStock = false; + switch (options.action) { case 'move': formTitle = '{% trans "Transfer Stock" %}'; actionTitle = '{% trans "Move" %}'; + specifyLocation = true; + allowSerializedStock = true; break; case 'count': formTitle = '{% trans "Count Stock" %}'; @@ -47,6 +52,7 @@ function adjustStock(items, options={}) { break; case 'delete': formTitle = '{% trans "Delete Stock" %}'; + allowSerializedStock = true; break; default: break; @@ -67,7 +73,15 @@ function adjustStock(items, options={}) { `; - items.forEach(function(item) { + var itemCount = 0; + + for (var idx = 0; idx < items.length; idx++) { + + var item = items[idx]; + + if ((item.serial != null) && !allowSerializedStock) { + continue; + } var pk = item.pk; @@ -150,7 +164,17 @@ function adjustStock(items, options={}) { ${buttons} `; - }); + itemCount += 1; + } + + if (itemCount == 0) { + showAlertDialog( + '{% trans "Select Stock Items" %}', + '{% trans "You must select at least one available stock item" %}', + ); + + return; + } html += ``; @@ -158,24 +182,32 @@ function adjustStock(items, options={}) { title: formTitle, }); - constructFormBody({}, { - fields: { - location: { - label: '{% trans "Location" %}', - help_text: '{% trans "Select stock location" %}', - type: 'related field', - required: true, - api_url: `/api/stock/location/`, - model: 'stocklocation', - }, - note: { - label: '{% trans "Notes" %}', - help_text: '{% trans "Stock transaction notes" %}', - type: 'string', - } + // 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', }, + note: { + label: '{% trans "Notes" %}', + help_text: '{% trans "Stock transaction notes" %}', + type: 'string', + } + }; + + if (!specifyLocation) { + delete extraFields.location; + } + + constructFormBody({}, { preFormContent: html, + fields: extraFields, confirm: true, + confirmMessage: '{% trans "Confirm stock adjustment" %}', modal: modal, onSubmit: function(fields, options) { console.log("submit!"); From e3f85414fa8d083d5a2a305a699453865b1a4549 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 17:32:06 +1000 Subject: [PATCH 11/26] Stock API URL cleanup --- InvenTree/stock/api.py | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 64be7c885f..b002484798 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -1093,47 +1093,41 @@ class LocationDetail(generics.RetrieveUpdateDestroyAPIView): serializer_class = LocationSerializer -stock_endpoints = [ - url(r'^$', StockDetail.as_view(), name='api-stock-detail'), -] - -location_endpoints = [ - url(r'^(?P\d+)/', LocationDetail.as_view(), name='api-location-detail'), - - url(r'^.*$', StockLocationList.as_view(), name='api-location-list'), -] - stock_api_urls = [ - url(r'location/', include(location_endpoints)), + url(r'^location/', include([ + url(r'^(?P\d+)/', LocationDetail.as_view(), name='api-location-detail'), + url(r'^.*$', StockLocationList.as_view(), name='api-location-list'), + ])), - # These JSON endpoints have been replaced (for now) with server-side form rendering - 02/06/2019 - # TODO: Remove server-side forms for stock adjustment!!! - url(r'count/?', StockCount.as_view(), name='api-stock-count'), - url(r'add/?', StockAdd.as_view(), name='api-stock-add'), - url(r'remove/?', StockRemove.as_view(), name='api-stock-remove'), - url(r'transfer/?', StockTransfer.as_view(), name='api-stock-transfer'), + # Endpoints for bulk stock adjustment actions + url(r'^count/', StockCount.as_view(), name='api-stock-count'), + url(r'^add/', StockAdd.as_view(), name='api-stock-add'), + url(r'^remove/', StockRemove.as_view(), name='api-stock-remove'), + url(r'^transfer/', StockTransfer.as_view(), name='api-stock-transfer'), - # Base URL for StockItemAttachment API endpoints + # StockItemAttachment API endpoints url(r'^attachment/', include([ url(r'^(?P\d+)/', StockAttachmentDetail.as_view(), name='api-stock-attachment-detail'), url(r'^$', StockAttachmentList.as_view(), name='api-stock-attachment-list'), ])), - # Base URL for StockItemTestResult API endpoints + # StockItemTestResult API endpoints url(r'^test/', include([ url(r'^(?P\d+)/', StockItemTestResultDetail.as_view(), name='api-stock-test-result-detail'), url(r'^.*$', StockItemTestResultList.as_view(), name='api-stock-test-result-list'), ])), + # StockItemTracking API endpoints url(r'^track/', include([ url(r'^(?P\d+)/', StockTrackingDetail.as_view(), name='api-stock-tracking-detail'), url(r'^.*$', StockTrackingList.as_view(), name='api-stock-tracking-list'), ])), - url(r'^tree/?', StockCategoryTree.as_view(), name='api-stock-tree'), + url(r'^tree/', StockCategoryTree.as_view(), name='api-stock-tree'), # Detail for a single stock item - url(r'^(?P\d+)/', include(stock_endpoints)), + url(r'^(?P\d+)/', StockDetail.as_view(), name='api-stock-detail'), + # Anything else url(r'^.*$', StockList.as_view(), name='api-stock-list'), ] From 0c41cc7c775164ce4ef57f4dcd3ec4b1577b8659 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 18:13:06 +1000 Subject: [PATCH 12/26] Handle form submissions --- InvenTree/templates/js/forms.js | 4 +++ InvenTree/templates/js/stock.js | 58 ++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 490b67944f..65c1a11b44 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -715,6 +715,10 @@ function getFormFieldValue(name, field, options) { // Find the HTML element var el = $(options.modal).find(`#id_${name}`); + if (!el) { + return null; + } + var value = null; switch (field.type) { diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 50bc49947f..8612e2758a 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -28,6 +28,9 @@ function adjustStock(items, options={}) { var formTitle = 'Form Title Here'; var actionTitle = null; + // API url + var url = null; + var specifyLocation = false; var allowSerializedStock = false; @@ -37,18 +40,22 @@ function adjustStock(items, options={}) { actionTitle = '{% trans "Move" %}'; specifyLocation = true; allowSerializedStock = true; + url = '{% url "api-stock-transfer" %}'; break; case 'count': formTitle = '{% trans "Count Stock" %}'; actionTitle = '{% trans "Count" %}'; + url = '{% url "api-stock-count" %}'; break; case 'take': formTitle = '{% trans "Remove Stock" %}'; actionTitle = '{% trans "Take" %}'; + url = '{% url "api-stock-remove" %}'; break; case 'add': formTitle = '{% trans "Add Stock" %}'; actionTitle = '{% trans "Add" %}'; + url = '{% url "api-stock-add" %}'; break; case 'delete': formTitle = '{% trans "Delete Stock" %}'; @@ -156,7 +163,7 @@ function adjustStock(items, options={}) { buttons += `
`; html += ` - + ${item.part_detail.full_name} ${quantity}${status} ${location} @@ -192,7 +199,7 @@ function adjustStock(items, options={}) { api_url: `/api/stock/location/`, model: 'stocklocation', }, - note: { + notes: { label: '{% trans "Notes" %}', help_text: '{% trans "Stock transaction notes" %}', type: 'string', @@ -209,8 +216,48 @@ function adjustStock(items, options={}) { confirm: true, confirmMessage: '{% trans "Confirm stock adjustment" %}', modal: modal, - onSubmit: function(fields, options) { - console.log("submit!"); + onSubmit: function(fields, opts) { + + // Data to transmit + var data = { + items: [], + }; + + // Add values for each selected stock item + items.forEach(function(item) { + + var q = getFormFieldValue(item.pk, {}, {modal: modal}); + + data.items.push({pk: item.pk, quantity: q}) + }); + + // Add in extra field data + for (field_name in extraFields) { + data[field_name] = getFormFieldValue( + field_name, + fields[field_name], + { + modal: modal, + } + ); + } + + inventreePut( + url, + data, + { + method: 'POST', + success: function(response, status) { + + // Destroy the modal window + $(modal).modal('hide'); + + if (options.onSuccess) { + options.onSuccess(); + } + } + } + ); } }); @@ -957,6 +1004,9 @@ function loadStockTable(table, options) { adjustStock(items, { action: action, + onSuccess: function() { + $('#stock-table').bootstrapTable('refresh'); + } }); return; From e04828214a5dbedf3901612049dbff21362d861c Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 19:20:29 +1000 Subject: [PATCH 13/26] Refactor showApiError() function --- InvenTree/stock/api.py | 6 ++-- InvenTree/templates/js/api.js | 48 ++++++++++++++++++++++++++++++++ InvenTree/templates/js/forms.js | 44 ++--------------------------- InvenTree/templates/js/modals.js | 7 +++-- InvenTree/templates/js/stock.js | 22 +++++++++++++++ 5 files changed, 79 insertions(+), 48 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index b002484798..384e5d1d71 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -244,17 +244,17 @@ class StockTransfer(StockAdjust): def post(self, request, *args, **kwargs): - self.get_items(request) - data = request.data try: location = StockLocation.objects.get(pk=data.get('location', None)) except (ValueError, StockLocation.DoesNotExist): - raise ValidationError({'location': 'Valid location must be specified'}) + raise ValidationError({'location': [_('Valid location must be specified')]}) n = 0 + self.get_items(request) + for item in self.items: # If quantity is not specified, move the entire stock diff --git a/InvenTree/templates/js/api.js b/InvenTree/templates/js/api.js index 5e8905a1dd..93fa5a41e4 100644 --- a/InvenTree/templates/js/api.js +++ b/InvenTree/templates/js/api.js @@ -1,3 +1,6 @@ +{% load i18n %} +{% load inventree_extras %} + var jQuery = window.$; // using jQuery @@ -138,4 +141,49 @@ function inventreeDelete(url, options={}) { inventreePut(url, {}, options); +} + + +function showApiError(xhr) { + + var title = null; + var message = null; + + switch (xhr.status) { + case 0: // No response + title = '{% trans "No Response" %}'; + message = '{% trans "No response from the InvenTree server" %}'; + break; + case 400: // Bad request + // Note: Normally error code 400 is handled separately, + // and should now be shown here! + title = '{% trans "Error 400: Bad request" %}'; + message = '{% trans "API request returned error code 400" %}'; + break; + case 401: // Not authenticated + title = '{% trans "Error 401: Not Authenticated" %}'; + message = '{% trans "Authentication credentials not supplied" %}'; + break; + case 403: // Permission denied + title = '{% trans "Error 403: Permission Denied" %}'; + message = '{% trans "You do not have the required permissions to access this function" %}'; + break; + case 404: // Resource not found + title = '{% trans "Error 404: Resource Not Found" %}'; + message = '{% trans "The requested resource could not be located on the server" %}'; + break; + case 408: // Timeout + title = '{% trans "Error 408: Timeout" %}'; + message = '{% trans "Connection timeout while requesting data from server" %}'; + break; + default: + title = '{% trans "Unhandled Error Code" %}'; + message = `{% trans "Error code" %}: ${xhr.status}`; + break; + } + + message += "
"; + message += renderErrorMessage(xhr); + + showAlertDialog(title, message); } \ No newline at end of file diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index 65c1a11b44..103ba26572 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -422,10 +422,8 @@ function constructFormBody(fields, options) { default: break; } - - var f = constructField(name, field, options); - html += f; + html += constructField(name, field, options); } // TODO: Dynamically create the modals, @@ -599,47 +597,9 @@ function submitFormData(fields, options) { case 400: // Bad request handleFormErrors(xhr.responseJSON, fields, options); break; - case 0: // No response - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "No Response" %}', - '{% trans "No response from the InvenTree server" %}', - ); - break; - case 401: // Not authenticated - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "Error 401: Not Authenticated" %}', - '{% trans "Authentication credentials not supplied" %}', - ); - break; - case 403: // Permission denied - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "Error 403: Permission Denied" %}', - '{% trans "You do not have the required permissions to access this function" %}', - ); - break; - case 404: // Resource not found - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "Error 404: Resource Not Found" %}', - '{% trans "The requested resource could not be located on the server" %}', - ); - break; - case 408: // Timeout - $(options.modal).modal('hide'); - showAlertDialog( - '{% trans "Error 408: Timeout" %}', - '{% trans "Connection timeout while requesting data from server" %}', - ); - break; default: $(options.modal).modal('hide'); - - showAlertDialog('{% trans "Error requesting form data" %}', renderErrorMessage(xhr)); - - console.log(`WARNING: Unhandled response code - ${xhr.status}`); + showApiError(xhr); break; } } diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index b613ed81f6..b49d7fadfc 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -39,12 +39,13 @@ function createNewModal(options={}) { {% endif %} @@ -221,8 +227,8 @@ }); {% if location %} - $("#location-count").click(function() { + function adjustLocationStock(action) { inventreeGet( '{% url "api-stock-list" %}', { @@ -233,7 +239,7 @@ }, { success: function(items) { - adjustStock('count', items, { + adjustStock(action, items, { onSuccess: function() { location.reload(); } @@ -241,6 +247,14 @@ } } ); + } + + $("#location-count").click(function() { + adjustLocationStock('count'); + }); + + $("#location-move").click(function() { + adjustLocationStock('move'); }); $('#print-label').click(function() { From a1579eecfd719235e6174bc1099369f804f30901 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 20:55:28 +1000 Subject: [PATCH 22/26] Refactor "showAlertDialog" function --- InvenTree/templates/js/modals.js | 27 ++++++++++++++------------- InvenTree/templates/modals.html | 23 +---------------------- 2 files changed, 15 insertions(+), 35 deletions(-) diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index b49d7fadfc..1b685cb2a8 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -103,6 +103,14 @@ function createNewModal(options={}) { modalSetSubmitText(modal_name, options.submitText || '{% trans "Submit" %}'); modalSetCloseText(modal_name, options.cancelText || '{% trans "Cancel" %}'); + if (options.hideSubmitButton) { + $(modal_name).find('#modal-form-submit').hide(); + } + + if (options.hideCloseButton) { + $(modal_name).find('#modal-form-cancel').hide(); + } + // Return the "name" of the modal return modal_name; } @@ -552,25 +560,18 @@ function showAlertDialog(title, content, options={}) { * * title - Title text * content - HTML content of the dialog window - * options: - * modal - modal form to use (default = '#modal-alert-dialog') */ - var modal = options.modal || '#modal-alert-dialog'; - $(modal).on('shown.bs.modal', function() { - $(modal + ' .modal-form-content').scrollTop(0); + var modal = createNewModal({ + title: title, + cancelText: '{% trans "Close" %}', + hideSubmitButton: true, }); - modalSetTitle(modal, title); - modalSetContent(modal, content); + modalSetContent(modal, content); - $(modal).modal({ - backdrop: 'static', - keyboard: false, - }); - - $(modal).modal('show'); + $(modal).modal('show'); } diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html index e2bd44554c..7b1c54bfb7 100644 --- a/InvenTree/templates/modals.html +++ b/InvenTree/templates/modals.html @@ -77,25 +77,4 @@ - - - \ No newline at end of file + \ No newline at end of file From edf4aab063d875d21b229db451481afda634f7ba Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 21:03:01 +1000 Subject: [PATCH 23/26] Refactor "showQuestionDialog" function --- InvenTree/templates/js/modals.js | 26 +++++--------------------- InvenTree/templates/modals.html | 21 --------------------- 2 files changed, 5 insertions(+), 42 deletions(-) diff --git a/InvenTree/templates/js/modals.js b/InvenTree/templates/js/modals.js index 1b685cb2a8..b404af364c 100644 --- a/InvenTree/templates/js/modals.js +++ b/InvenTree/templates/js/modals.js @@ -12,7 +12,6 @@ */ function createNewModal(options={}) { - var id = 1; // Check out what modal forms are already being displayed @@ -588,22 +587,15 @@ function showQuestionDialog(title, content, options={}) { * cancel - Functino to run if the user presses 'Cancel' */ - var modal = options.modal || '#modal-question-dialog'; - - $(modal).on('shown.bs.modal', function() { - $(modal + ' .modal-form-content').scrollTop(0); + var modal = createNewModal({ + title: title, + submitText: options.accept_text || '{% trans "Accept" %}', + cancelText: options.cancel_text || '{% trans "Cancel" %}', }); - modalSetTitle(modal, title); modalSetContent(modal, content); - var accept_text = options.accept_text || '{% trans "Accept" %}'; - var cancel_text = options.cancel_text || '{% trans "Cancel" %}'; - - $(modal).find('#modal-form-cancel').html(cancel_text); - $(modal).find('#modal-form-accept').html(accept_text); - - $(modal).on('click', '#modal-form-accept', function() { + $(modal).on('click', "#modal-form-submit", function() { $(modal).modal('hide'); if (options.accept) { @@ -611,14 +603,6 @@ function showQuestionDialog(title, content, options={}) { } }); - $(modal).on('click', 'modal-form-cancel', function() { - $(modal).modal('hide'); - - if (options.cancel) { - options.cancel(); - } - }); - $(modal).modal('show'); } diff --git a/InvenTree/templates/modals.html b/InvenTree/templates/modals.html index 7b1c54bfb7..11ddc40938 100644 --- a/InvenTree/templates/modals.html +++ b/InvenTree/templates/modals.html @@ -56,25 +56,4 @@ - - - - \ No newline at end of file From 52eedef82009c3d267fc3b4b33ec33a3542ec894 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 21:03:28 +1000 Subject: [PATCH 24/26] remove old StockAdjust view --- InvenTree/stock/urls.py | 2 - InvenTree/stock/views.py | 337 --------------------------------------- 2 files changed, 339 deletions(-) diff --git a/InvenTree/stock/urls.py b/InvenTree/stock/urls.py index ac9474f805..67101c1f3b 100644 --- a/InvenTree/stock/urls.py +++ b/InvenTree/stock/urls.py @@ -64,8 +64,6 @@ stock_urls = [ url(r'^track/', include(stock_tracking_urls)), - url(r'^adjust/?', views.StockAdjust.as_view(), name='stock-adjust'), - url(r'^export-options/?', views.StockExportOptions.as_view(), name='stock-export-options'), url(r'^export/?', views.StockExport.as_view(), name='stock-export'), diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py index c74b0bb2fc..6713909a2a 100644 --- a/InvenTree/stock/views.py +++ b/InvenTree/stock/views.py @@ -749,343 +749,6 @@ class StockItemUninstall(AjaxView, FormMixin): return context -class StockAdjust(AjaxView, FormMixin): - """ View for enacting simple stock adjustments: - - - Take items from stock - - Add items to stock - - Count items - - Move stock - - Delete stock items - - """ - - ajax_template_name = 'stock/stock_adjust.html' - ajax_form_title = _('Adjust Stock') - form_class = StockForms.AdjustStockForm - stock_items = [] - role_required = 'stock.change' - - def get_GET_items(self): - """ Return list of stock items initally requested using GET. - - Items can be retrieved by: - - a) List of stock ID - stock[]=1,2,3,4,5 - b) Parent part - part=3 - c) Parent location - location=78 - d) Single item - item=2 - """ - - # Start with all 'in stock' items - items = StockItem.objects.filter(StockItem.IN_STOCK_FILTER) - - # Client provides a list of individual stock items - if 'stock[]' in self.request.GET: - items = items.filter(id__in=self.request.GET.getlist('stock[]')) - - # Client provides a PART reference - elif 'part' in self.request.GET: - items = items.filter(part=self.request.GET.get('part')) - - # Client provides a LOCATION reference - elif 'location' in self.request.GET: - items = items.filter(location=self.request.GET.get('location')) - - # Client provides a single StockItem lookup - elif 'item' in self.request.GET: - items = [StockItem.objects.get(id=self.request.GET.get('item'))] - - # Unsupported query (no items) - else: - items = [] - - for item in items: - - # Initialize quantity to zero for addition/removal - if self.stock_action in ['take', 'add']: - item.new_quantity = 0 - # Initialize quantity at full amount for counting or moving - else: - item.new_quantity = item.quantity - - return items - - def get_POST_items(self): - """ Return list of stock items sent back by client on a POST request """ - - items = [] - - for item in self.request.POST: - if item.startswith('stock-id-'): - - pk = item.replace('stock-id-', '') - q = self.request.POST[item] - - try: - stock_item = StockItem.objects.get(pk=pk) - except StockItem.DoesNotExist: - continue - - stock_item.new_quantity = q - - items.append(stock_item) - - return items - - def get_stock_action_titles(self): - - # Choose form title and action column based on the action - titles = { - 'move': [_('Move Stock Items'), _('Move')], - 'count': [_('Count Stock Items'), _('Count')], - 'take': [_('Remove From Stock'), _('Take')], - 'add': [_('Add Stock Items'), _('Add')], - 'delete': [_('Delete Stock Items'), _('Delete')], - } - - self.ajax_form_title = titles[self.stock_action][0] - self.stock_action_title = titles[self.stock_action][1] - - def get_context_data(self): - - context = super().get_context_data() - - context['stock_items'] = self.stock_items - - context['stock_action'] = self.stock_action.strip().lower() - - self.get_stock_action_titles() - context['stock_action_title'] = self.stock_action_title - - # Quantity column will be read-only in some circumstances - context['edit_quantity'] = not self.stock_action == 'delete' - - return context - - def get_form(self): - - form = super().get_form() - - if not self.stock_action == 'move': - form.fields.pop('destination') - form.fields.pop('set_loc') - - return form - - def get(self, request, *args, **kwargs): - - self.request = request - - # Action - self.stock_action = request.GET.get('action', '').lower() - - # Pick a default action... - if self.stock_action not in ['move', 'count', 'take', 'add', 'delete']: - self.stock_action = 'count' - - # Save list of items! - self.stock_items = self.get_GET_items() - - return self.renderJsonResponse(request, self.get_form()) - - def post(self, request, *args, **kwargs): - - self.request = request - - self.stock_action = request.POST.get('stock_action', 'invalid').strip().lower() - - # Update list of stock items - self.stock_items = self.get_POST_items() - - form = self.get_form() - - valid = form.is_valid() - - for item in self.stock_items: - - try: - item.new_quantity = Decimal(item.new_quantity) - except ValueError: - item.error = _('Must enter integer value') - valid = False - continue - - if item.new_quantity < 0: - item.error = _('Quantity must be positive') - valid = False - continue - - if self.stock_action in ['move', 'take']: - - if item.new_quantity > item.quantity: - item.error = _('Quantity must not exceed {x}').format(x=item.quantity) - valid = False - continue - - confirmed = str2bool(request.POST.get('confirm')) - - if not confirmed: - valid = False - form.add_error('confirm', _('Confirm stock adjustment')) - - data = { - 'form_valid': valid, - } - - if valid: - result = self.do_action(note=form.cleaned_data['note']) - - data['success'] = result - - # Special case - Single Stock Item - # If we deplete the stock item, we MUST redirect to a new view - single_item = len(self.stock_items) == 1 - - if result and single_item: - - # Was the entire stock taken? - item = self.stock_items[0] - - if item.quantity == 0: - # Instruct the form to redirect - data['url'] = reverse('stock-index') - - return self.renderJsonResponse(request, form, data=data, context=self.get_context_data()) - - def do_action(self, note=None): - """ Perform stock adjustment action """ - - if self.stock_action == 'move': - destination = None - - set_default_loc = str2bool(self.request.POST.get('set_loc', False)) - - try: - destination = StockLocation.objects.get(id=self.request.POST.get('destination')) - except StockLocation.DoesNotExist: - pass - except ValueError: - pass - - return self.do_move(destination, set_default_loc, note=note) - - elif self.stock_action == 'add': - return self.do_add(note=note) - - elif self.stock_action == 'take': - return self.do_take(note=note) - - elif self.stock_action == 'count': - return self.do_count(note=note) - - elif self.stock_action == 'delete': - return self.do_delete(note=note) - - else: - return _('No action performed') - - def do_add(self, note=None): - - count = 0 - - for item in self.stock_items: - if item.new_quantity <= 0: - continue - - item.add_stock(item.new_quantity, self.request.user, notes=note) - - count += 1 - - return _('Added stock to {n} items').format(n=count) - - def do_take(self, note=None): - - count = 0 - - for item in self.stock_items: - if item.new_quantity <= 0: - continue - - item.take_stock(item.new_quantity, self.request.user, notes=note) - - count += 1 - - return _('Removed stock from {n} items').format(n=count) - - def do_count(self, note=None): - - count = 0 - - for item in self.stock_items: - - item.stocktake(item.new_quantity, self.request.user, notes=note) - - count += 1 - - return _("Counted stock for {n} items".format(n=count)) - - def do_move(self, destination, set_loc=None, note=None): - """ Perform actual stock movement """ - - count = 0 - - for item in self.stock_items: - # Avoid moving zero quantity - if item.new_quantity <= 0: - continue - - # If we wish to set the destination location to the default one - if set_loc: - item.part.default_location = destination - item.part.save() - - # Do not move to the same location (unless the quantity is different) - if destination == item.location and item.new_quantity == item.quantity: - continue - - item.move(destination, note, self.request.user, quantity=item.new_quantity) - - count += 1 - - # Is ownership control enabled? - stock_ownership_control = InvenTreeSetting.get_setting('STOCK_OWNERSHIP_CONTROL') - - if stock_ownership_control: - # Fetch destination owner - destination_owner = destination.owner - - if destination_owner: - # Update owner - item.owner = destination_owner - item.save() - - if count == 0: - return _('No items were moved') - - else: - return _('Moved {n} items to {dest}').format( - n=count, - dest=destination.pathstring) - - def do_delete(self): - """ Delete multiple stock items """ - - count = 0 - # note = self.request.POST['note'] - - for item in self.stock_items: - - # TODO - In the future, StockItems should not be 'deleted' - # TODO - Instead, they should be marked as "inactive" - - item.delete() - - count += 1 - - return _("Deleted {n} stock items").format(n=count) - - class StockItemEdit(AjaxUpdateView): """ View for editing details of a single StockItem From 77cfadad42fbcaccac680dc6d168e406b2f087ee Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 21:11:29 +1000 Subject: [PATCH 25/26] Add 'title' option for contsructed fields --- InvenTree/stock/api.py | 4 ---- InvenTree/templates/js/forms.js | 5 +++++ InvenTree/templates/js/stock.js | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 91810dddc6..08e948607a 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -120,9 +120,6 @@ class StockAdjust(APIView): - StockAdd: add stock items - StockRemove: remove stock items - StockTransfer: transfer stock items - - # TODO - This needs serious refactoring!!! - """ queryset = StockItem.objects.none() @@ -502,7 +499,6 @@ class StockList(generics.ListCreateAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - # TODO - Save the user who created this item item = serializer.save() # A location was *not* specified - try to infer it diff --git a/InvenTree/templates/js/forms.js b/InvenTree/templates/js/forms.js index a9747d8f7d..b71551747c 100644 --- a/InvenTree/templates/js/forms.js +++ b/InvenTree/templates/js/forms.js @@ -1473,6 +1473,11 @@ function constructInputOptions(name, classes, type, parameters) { opts.push(`required=''`); } + // Custom mouseover title? + if (parameters.title != null) { + opts.push(`title='${parameters.title}'`); + } + // Placeholder? if (parameters.placeholder != null) { opts.push(`placeholder='${parameters.placeholder}'`); diff --git a/InvenTree/templates/js/stock.js b/InvenTree/templates/js/stock.js index 6ce4c3aae4..f173f868f7 100644 --- a/InvenTree/templates/js/stock.js +++ b/InvenTree/templates/js/stock.js @@ -147,6 +147,7 @@ function adjustStock(action, items, options={}) { min_value: minValue, max_value: maxValue, read_only: readonly, + title: readonly ? '{% trans "Quantity cannot be adjusted for serialized stock" %}' : '{% trans "Specify stock quantity" %}', } ) }; From 30fd3c8841f0e35020e126565d114d2b0e0dd131 Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 12 Jul 2021 22:03:36 +1000 Subject: [PATCH 26/26] Unit test fixes --- InvenTree/stock/test_api.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py index 729bf25a9b..74f9505c4a 100644 --- a/InvenTree/stock/test_api.py +++ b/InvenTree/stock/test_api.py @@ -7,8 +7,8 @@ from __future__ import unicode_literals from datetime import datetime, timedelta -from rest_framework import status from django.urls import reverse +from rest_framework import status from InvenTree.status_codes import StockStatus from InvenTree.api_tester import InvenTreeAPITestCase @@ -456,30 +456,32 @@ class StocktakeTest(StockAPITestCase): # POST without a PK response = self.post(url, data) - self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'must contain a valid integer primary-key', status_code=status.HTTP_400_BAD_REQUEST) - # POST with a PK but no quantity + # POST with an invalid PK data['items'] = [{ 'pk': 10 }] response = self.post(url, data) - self.assertContains(response, 'must contain a valid pk', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'does not match valid stock item', status_code=status.HTTP_400_BAD_REQUEST) + # POST with missing quantity value data['items'] = [{ 'pk': 1234 }] response = self.post(url, data) - self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'Invalid quantity value', status_code=status.HTTP_400_BAD_REQUEST) + # POST with an invalid quantity value data['items'] = [{ 'pk': 1234, 'quantity': '10x0d' }] response = self.post(url, data) - self.assertContains(response, 'must contain a valid quantity', status_code=status.HTTP_400_BAD_REQUEST) + self.assertContains(response, 'Invalid quantity value', status_code=status.HTTP_400_BAD_REQUEST) data['items'] = [{ 'pk': 1234,