@@ -514,6 +520,14 @@ $("#barcode-scan-into-location").click(function() {
});
});
+{% if plugins_enabled %}
+$('#locate-item-button').click(function() {
+ locateItemOrLocation({
+ item: {{ item.pk }},
+ });
+});
+{% endif %}
+
function itemAdjust(action) {
inventreeGet(
diff --git a/InvenTree/stock/templates/stock/location.html b/InvenTree/stock/templates/stock/location.html
index 61320a2676..1066adf6ea 100644
--- a/InvenTree/stock/templates/stock/location.html
+++ b/InvenTree/stock/templates/stock/location.html
@@ -1,6 +1,7 @@
{% extends "stock/stock_app_base.html" %}
{% load static %}
{% load inventree_extras %}
+{% load plugin_extras %}
{% load i18n %}
{% block sidebar %}
@@ -27,6 +28,14 @@
{% include "admin_button.html" with url=url %}
{% endif %}
+{% mixin_available "locate" as locate_available %}
+{% if location and plugins_enabled and locate_available %}
+
+
+{% endif %}
+
{% if barcodes %}
{% if location %}
@@ -206,6 +215,14 @@
{% block js_ready %}
{{ block.super }}
+ {% if plugins_enabled and location %}
+ $('#locate-location-button').click(function() {
+ locateItemOrLocation({
+ location: {{ location.pk }},
+ });
+ });
+ {% endif %}
+
onPanelLoad('sublocations', function() {
loadStockLocationTable($('#sublocation-table'), {
params: {
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index 1f040b008d..28a8e0de0b 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -2,9 +2,6 @@
Unit testing for the Stock API
"""
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
import os
import io
import tablib
diff --git a/InvenTree/stock/views.py b/InvenTree/stock/views.py
index 168403d692..15191b5fb7 100644
--- a/InvenTree/stock/views.py
+++ b/InvenTree/stock/views.py
@@ -2,9 +2,6 @@
Django views for interacting with Stock app
"""
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
from datetime import datetime
from django.views.generic import DetailView, ListView
diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html
index 0d8272892a..795a5679aa 100644
--- a/InvenTree/templates/base.html
+++ b/InvenTree/templates/base.html
@@ -2,6 +2,7 @@
{% load i18n %}
{% load inventree_extras %}
+{% plugins_enabled as plugins_enabled %}
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %}
{% settings_value "REPORT_ENABLE" as report_enabled %}
diff --git a/InvenTree/templates/js/translated/bom.js b/InvenTree/templates/js/translated/bom.js
index 9bd66877da..0119b36664 100644
--- a/InvenTree/templates/js/translated/bom.js
+++ b/InvenTree/templates/js/translated/bom.js
@@ -16,6 +16,7 @@
/* exported
constructBomUploadTable,
+ deleteBomItems,
downloadBomTemplate,
exportBom,
newPartFromBomWizard,
@@ -647,7 +648,88 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
reloadParentTable();
}
});
+}
+
+function deleteBomItems(items, options={}) {
+ /* Delete the selected BOM items from the database
+ */
+
+ function renderItem(item, opts={}) {
+
+ var sub_part = item.sub_part_detail;
+ var thumb = thumbnailImage(sub_part.thumbnail || sub_part.image);
+
+ var html = `
+
+ ${thumb} ${sub_part.full_name} |
+ ${item.reference} |
+ ${item.quantity}
+ |
+ `;
+
+ return html;
+ }
+
+ var rows = '';
+
+ items.forEach(function(item) {
+ rows += renderItem(item);
+ });
+
+ var html = `
+
+ {% trans "All selected BOM items will be deleted" %}
+
+
+
+
+ {% trans "Part" %} |
+ {% trans "Reference" %} |
+ {% trans "Quantity" %} |
+
+ ${rows}
+
+ `;
+
+ constructFormBody({}, {
+ title: '{% trans "Delete selected BOM items?" %}',
+ fields: {},
+ preFormContent: html,
+ submitText: '{% trans "Delete" %}',
+ submitClass: 'danger',
+ confirm: true,
+ onSubmit: function(fields, opts) {
+ // Individually send DELETE requests for each BOM item
+ // We do *not* send these all at once, to prevent overloading the server
+
+ // Show the progress spinner
+ $(opts.modal).find('#modal-progress-spinner').show();
+
+ function deleteNextBomItem() {
+
+ if (items.length > 0) {
+
+ var item = items.shift();
+
+ inventreeDelete(`/api/bom/${item.pk}/`,
+ {
+ complete: deleteNextBomItem,
+ }
+ );
+ } else {
+ // Destroy this modal once all items are deleted
+ $(opts.modal).modal('hide');
+
+ if (options.success) {
+ options.success();
+ }
+ }
+ }
+
+ deleteNextBomItem();
+ },
+ });
}
@@ -1146,19 +1228,13 @@ function loadBomTable(table, options={}) {
var pk = $(this).attr('pk');
- var html = `
-
- {% trans "Are you sure you want to delete this BOM item?" %}
-
`;
+ var item = table.bootstrapTable('getRowByUniqueId', pk);
- constructForm(`/api/bom/${pk}/`, {
- method: 'DELETE',
- title: '{% trans "Delete BOM Item" %}',
- preFormContent: html,
- onSuccess: function() {
+ deleteBomItems([item], {
+ success: function() {
reloadBomTable(table);
}
- });
+ });
});
// Callback for "edit" button
diff --git a/InvenTree/templates/js/translated/forms.js b/InvenTree/templates/js/translated/forms.js
index 642523a60b..d45a2de78c 100644
--- a/InvenTree/templates/js/translated/forms.js
+++ b/InvenTree/templates/js/translated/forms.js
@@ -288,6 +288,7 @@ function constructDeleteForm(fields, options) {
* - method: The HTTP method e.g. 'PUT', 'POST', 'DELETE' (default='PATCH')
* - title: The form title
* - submitText: Text for the "submit" button
+ * - submitClass: CSS class for the "submit" button (default = ')
* - closeText: Text for the "close" button
* - fields: list of fields to display, with the following options
* - filters: API query filters
diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js
index d19c403861..388509c8bf 100644
--- a/InvenTree/templates/js/translated/label.js
+++ b/InvenTree/templates/js/translated/label.js
@@ -236,17 +236,13 @@ function selectLabel(labels, items, options={}) {
if (plugins_enabled) {
inventreeGet(
`/api/plugin/`,
- {},
+ {
+ mixin: 'labels',
+ },
{
async: false,
success: function(response) {
- response.forEach(function(plugin) {
- // Look for active plugins which implement the 'labels' mixin class
- if (plugin.active && plugin.mixins && plugin.mixins.labels) {
- // This plugin supports label printing
- plugins.push(plugin);
- }
- });
+ plugins = response;
}
}
);
diff --git a/InvenTree/templates/js/translated/modals.js b/InvenTree/templates/js/translated/modals.js
index 85f503682e..464006ae12 100644
--- a/InvenTree/templates/js/translated/modals.js
+++ b/InvenTree/templates/js/translated/modals.js
@@ -42,6 +42,8 @@ function createNewModal(options={}) {
}
});
+ var submitClass = options.submitClass || 'primary';
+
var html = `
diff --git a/InvenTree/templates/js/translated/order.js b/InvenTree/templates/js/translated/order.js
index 372dc70a9a..e2bee865fd 100644
--- a/InvenTree/templates/js/translated/order.js
+++ b/InvenTree/templates/js/translated/order.js
@@ -1568,23 +1568,10 @@ function loadPurchaseOrderLineItemTable(table, options={}) {
$(table).find('.button-line-edit').click(function() {
var pk = $(this).attr('pk');
+ var fields = poLineItemFields(options);
+
constructForm(`/api/order/po-line/${pk}/`, {
- fields: {
- part: {
- filters: {
- part_detail: true,
- supplier_detail: true,
- supplier: options.supplier,
- }
- },
- quantity: {},
- reference: {},
- purchase_price: {},
- purchase_price_currency: {},
- target_date: {},
- destination: {},
- notes: {},
- },
+ fields: fields,
title: '{% trans "Edit Line Item" %}',
onSuccess: function() {
$(table).bootstrapTable('refresh');
diff --git a/InvenTree/templates/js/translated/plugin.js b/InvenTree/templates/js/translated/plugin.js
index c612dd1e8c..62555c8ff4 100644
--- a/InvenTree/templates/js/translated/plugin.js
+++ b/InvenTree/templates/js/translated/plugin.js
@@ -7,6 +7,7 @@
/* exported
installPlugin,
+ locateItemOrLocation
*/
function installPlugin() {
@@ -24,3 +25,50 @@ function installPlugin() {
}
});
}
+
+
+function locateItemOrLocation(options={}) {
+
+ if (!options.item && !options.location) {
+ console.error(`locateItemOrLocation: Either 'item' or 'location' must be provided!`);
+ return;
+ }
+
+ function performLocate(plugin) {
+ inventreePut(
+ '{% url "api-locate-plugin" %}',
+ {
+ plugin: plugin,
+ item: options.item,
+ location: options.location,
+ },
+ {
+ method: 'POST',
+ },
+ );
+ }
+
+ // Request the list of available 'locate' plugins
+ inventreeGet(
+ `/api/plugin/`,
+ {
+ mixin: 'locate',
+ },
+ {
+ success: function(plugins) {
+ // No 'locate' plugins are available!
+ if (plugins.length == 0) {
+ console.warn(`No 'locate' plugins are available`);
+ } else if (plugins.length == 1) {
+ // Only a single locate plugin is available
+ performLocate(plugins[0].key);
+ } else {
+ // More than 1 location plugin available
+ // Select from a list
+ }
+ }
+ },
+ );
+}
+
+
diff --git a/ci/check_api_endpoint.py b/ci/check_api_endpoint.py
index 2969c64792..0d110379ef 100644
--- a/ci/check_api_endpoint.py
+++ b/ci/check_api_endpoint.py
@@ -2,9 +2,6 @@
Test that the root API endpoint is available.
"""
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
import json
import requests
diff --git a/ci/check_js_templates.py b/ci/check_js_templates.py
index 71b9912f3a..4f35e57eb4 100644
--- a/ci/check_js_templates.py
+++ b/ci/check_js_templates.py
@@ -7,9 +7,6 @@ This is because the "translated" javascript files are compiled into the "static"
They should only contain template tags that render static information.
"""
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
import sys
import re
import os
diff --git a/ci/check_locale_files.py b/ci/check_locale_files.py
index 06246cd923..d17fe27e3d 100644
--- a/ci/check_locale_files.py
+++ b/ci/check_locale_files.py
@@ -1,8 +1,5 @@
""" Check that there are no database migration files which have not been committed. """
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
import sys
import subprocess
diff --git a/ci/check_migration_files.py b/ci/check_migration_files.py
index 8ef0ada13d..16bd87485d 100644
--- a/ci/check_migration_files.py
+++ b/ci/check_migration_files.py
@@ -1,8 +1,5 @@
""" Check that there are no database migration files which have not been committed. """
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
import sys
import subprocess
diff --git a/ci/check_version_number.py b/ci/check_version_number.py
index 0514854407..732102ff52 100644
--- a/ci/check_version_number.py
+++ b/ci/check_version_number.py
@@ -2,9 +2,6 @@
On release, ensure that the release tag matches the InvenTree version number!
"""
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
import sys
import re
import os
@@ -25,7 +22,7 @@ if __name__ == '__main__':
# Extract the InvenTree software version
results = re.findall(r'INVENTREE_SW_VERSION = "(.*)"', text)
- if not len(results) == 1:
+ if len(results) != 1:
print(f"Could not find INVENTREE_SW_VERSION in {version_file}")
sys.exit(1)
@@ -91,7 +88,7 @@ if __name__ == '__main__':
sys.exit(1)
if args.tag:
- if not args.tag == version:
+ if args.tag != version:
print(f"Release tag '{args.tag}' does not match INVENTREE_SW_VERSION '{version}'")
sys.exit(1)
diff --git a/requirements.txt b/requirements.txt
index 5065b4f877..43d077104f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -36,6 +36,7 @@ flake8==3.8.3 # PEP checking
gunicorn>=20.1.0 # Gunicorn web server
importlib_metadata # Backport for importlib.metadata
inventree # Install the latest version of the InvenTree API python library
+isort==5.10.1 # DEV: python import sorting
markdown==3.3.4 # Force particular version of markdown
pep8-naming==0.11.1 # PEP naming convention extension
pillow==9.0.1 # Image manipulation
diff --git a/setup.cfg b/setup.cfg
index b4b0af8836..a483481f5d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,3 +20,11 @@ max-complexity = 20
[coverage:run]
source = ./InvenTree
+
+[isort]
+src_paths=InvenTree
+skip_glob =*/migrations/*.py
+known_django=django
+import_heading_firstparty=InvenTree imports
+import_heading_thirdparty=Third-Party imports
+sections=FUTURE, STDLIB, DJANGO, THIRDPARTY, FIRSTPARTY, LOCALFOLDER
diff --git a/tasks.py b/tasks.py
index 2ed0b4d35e..fc69c8ba22 100644
--- a/tasks.py
+++ b/tasks.py
@@ -6,11 +6,7 @@ import sys
import pathlib
import re
-
-try:
- from invoke import ctask as task
-except:
- from invoke import task
+from invoke import task
def apps():