diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 9d901516d5..e58ef798fc 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -2,9 +2,6 @@ Main JSON interface views """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.utils.translation import gettext_lazy as _ from django.conf import settings from django.http import JsonResponse diff --git a/InvenTree/InvenTree/fields.py b/InvenTree/InvenTree/fields.py index b3d81d75a5..894b993e50 100644 --- a/InvenTree/InvenTree/fields.py +++ b/InvenTree/InvenTree/fields.py @@ -1,7 +1,5 @@ """ Custom fields used in InvenTree """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import sys from .validators import allowable_url_schemes diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py index 8272673fc3..f0058e399a 100644 --- a/InvenTree/InvenTree/filters.py +++ b/InvenTree/InvenTree/filters.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from rest_framework.filters import OrderingFilter diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py index 8814c571dd..40e8509411 100644 --- a/InvenTree/InvenTree/forms.py +++ b/InvenTree/InvenTree/forms.py @@ -2,8 +2,6 @@ Helper forms which subclass Django forms to provide additional functionality """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from urllib.parse import urlencode import logging diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py index ae520e9dcb..85c5a4c8cc 100644 --- a/InvenTree/InvenTree/metadata.py +++ b/InvenTree/InvenTree/metadata.py @@ -1,7 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals - import logging from rest_framework import serializers diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py index 92db2012f8..73b2bf4572 100644 --- a/InvenTree/InvenTree/models.py +++ b/InvenTree/InvenTree/models.py @@ -2,8 +2,6 @@ Generic models which provide extra functionality over base Django model types. """ -from __future__ import unicode_literals - import re import os import logging diff --git a/InvenTree/InvenTree/permissions.py b/InvenTree/InvenTree/permissions.py index defb370435..920e111ce2 100644 --- a/InvenTree/InvenTree/permissions.py +++ b/InvenTree/InvenTree/permissions.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from rest_framework import permissions import users.models diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py index d3d0038cea..e16a4f1ba7 100644 --- a/InvenTree/InvenTree/serializers.py +++ b/InvenTree/InvenTree/serializers.py @@ -2,9 +2,6 @@ Serializers used in various InvenTree apps """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os import tablib diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 49a4049be5..77f55df271 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import re import json +import warnings import requests import logging @@ -11,6 +9,8 @@ from django.utils import timezone from django.core.exceptions import AppRegistryNotReady from django.db.utils import OperationalError, ProgrammingError +from django.core import mail as django_mail +from django.conf import settings logger = logging.getLogger("inventree") @@ -52,6 +52,15 @@ def schedule_task(taskname, **kwargs): pass +def raise_warning(msg): + """Log and raise a warning""" + logger.warning(msg) + + # If testing is running raise a warning that can be asserted + if settings.TESTING: + warnings.warn(msg) + + def offload_task(taskname, *args, force_sync=False, **kwargs): """ Create an AsyncTask if workers are running. @@ -67,28 +76,38 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): import importlib from InvenTree.status import is_worker_running + except AppRegistryNotReady: # pragma: no cover + logger.warning(f"Could not offload task '{taskname}' - app registry not ready") + return + except (OperationalError, ProgrammingError): # pragma: no cover + raise_warning(f"Could not offload task '{taskname}' - database not ready") - if is_worker_running() and not force_sync: # pragma: no cover - # Running as asynchronous task - try: - task = AsyncTask(taskname, *args, **kwargs) - task.run() - except ImportError: - logger.warning(f"WARNING: '{taskname}' not started - Function not found") + if is_worker_running() and not force_sync: # pragma: no cover + # Running as asynchronous task + try: + task = AsyncTask(taskname, *args, **kwargs) + task.run() + except ImportError: + raise_warning(f"WARNING: '{taskname}' not started - Function not found") + else: + + if callable(taskname): + # function was passed - use that + _func = taskname else: # Split path try: app, mod, func = taskname.split('.') app_mod = app + '.' + mod except ValueError: - logger.warning(f"WARNING: '{taskname}' not started - Malformed function path") + raise_warning(f"WARNING: '{taskname}' not started - Malformed function path") return # Import module from app try: _mod = importlib.import_module(app_mod) except ModuleNotFoundError: - logger.warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'") + raise_warning(f"WARNING: '{taskname}' not started - No module named '{app_mod}'") return # Retrieve function @@ -102,17 +121,11 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): if not _func: _func = eval(func) # pragma: no cover except NameError: - logger.warning(f"WARNING: '{taskname}' not started - No function named '{func}'") + raise_warning(f"WARNING: '{taskname}' not started - No function named '{func}'") return - # Workers are not running: run it as synchronous task - _func(*args, **kwargs) - - except AppRegistryNotReady: # pragma: no cover - logger.warning(f"Could not offload task '{taskname}' - app registry not ready") - return - except (OperationalError, ProgrammingError): # pragma: no cover - logger.warning(f"Could not offload task '{taskname}' - database not ready") + # Workers are not running: run it as synchronous task + _func(*args, **kwargs) def heartbeat(): @@ -126,8 +139,8 @@ def heartbeat(): try: from django_q.models import Success - logger.info("Could not perform heartbeat task - App registry not ready") except AppRegistryNotReady: # pragma: no cover + logger.info("Could not perform heartbeat task - App registry not ready") return threshold = timezone.now() - timedelta(minutes=30) @@ -205,25 +218,25 @@ def check_for_updates(): response = requests.get('https://api.github.com/repos/inventree/inventree/releases/latest') if response.status_code != 200: - raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}') + raise ValueError(f'Unexpected status code from GitHub API: {response.status_code}') # pragma: no cover data = json.loads(response.text) tag = data.get('tag_name', None) if not tag: - raise ValueError("'tag_name' missing from GitHub response") + raise ValueError("'tag_name' missing from GitHub response") # pragma: no cover match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag) - if len(match.groups()) != 3: + if len(match.groups()) != 3: # pragma: no cover logger.warning(f"Version '{tag}' did not match expected pattern") return latest_version = [int(x) for x in match.groups()] if len(latest_version) != 3: - raise ValueError(f"Version '{tag}' is not correct format") + raise ValueError(f"Version '{tag}' is not correct format") # pragma: no cover logger.info(f"Latest InvenTree version: '{tag}'") @@ -288,7 +301,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None): recipients = [recipients] offload_task( - 'django.core.mail.send_mail', + django_mail.send_mail, subject, body, from_email, diff --git a/InvenTree/InvenTree/test_tasks.py b/InvenTree/InvenTree/test_tasks.py index e9c9d9f01c..c8497e2241 100644 --- a/InvenTree/InvenTree/test_tasks.py +++ b/InvenTree/InvenTree/test_tasks.py @@ -2,10 +2,20 @@ Unit tests for task management """ +from datetime import timedelta + +from django.utils import timezone from django.test import TestCase from django_q.models import Schedule +from error_report.models import Error + import InvenTree.tasks +from common.models import InvenTreeSetting + + +threshold = timezone.now() - timedelta(days=30) +threshold_low = threshold - timedelta(days=1) class ScheduledTaskTests(TestCase): @@ -41,3 +51,79 @@ class ScheduledTaskTests(TestCase): # But the 'minutes' should have been updated t = Schedule.objects.get(func=task) self.assertEqual(t.minutes, 5) + + +def get_result(): + """Demo function for test_offloading""" + return 'abc' + + +class InvenTreeTaskTests(TestCase): + """Unit tests for tasks""" + + def test_offloading(self): + """Test task offloading""" + + # Run with function ref + InvenTree.tasks.offload_task(get_result) + + # Run with string ref + InvenTree.tasks.offload_task('InvenTree.test_tasks.get_result') + + # Error runs + # Malformed taskname + with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree' not started - Malformed function path"): + InvenTree.tasks.offload_task('InvenTree') + + # Non exsistent app + with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTreeABC.test_tasks.doesnotmatter' not started - No module named 'InvenTreeABC.test_tasks'"): + InvenTree.tasks.offload_task('InvenTreeABC.test_tasks.doesnotmatter') + + # Non exsistent function + with self.assertWarnsMessage(UserWarning, "WARNING: 'InvenTree.test_tasks.doesnotexsist' not started - No function named 'doesnotexsist'"): + InvenTree.tasks.offload_task('InvenTree.test_tasks.doesnotexsist') + + def test_task_hearbeat(self): + """Test the task heartbeat""" + InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat) + + def test_task_delete_successful_tasks(self): + """Test the task delete_successful_tasks""" + from django_q.models import Success + + Success.objects.create(name='abc', func='abc', stopped=threshold, started=threshold_low) + InvenTree.tasks.offload_task(InvenTree.tasks.delete_successful_tasks) + results = Success.objects.filter(started__lte=threshold) + self.assertEqual(len(results), 0) + + def test_task_delete_old_error_logs(self): + """Test the task delete_old_error_logs""" + + # Create error + error_obj = Error.objects.create() + error_obj.when = threshold_low + error_obj.save() + + # Check that it is not empty + errors = Error.objects.filter(when__lte=threshold,) + self.assertNotEqual(len(errors), 0) + + # Run action + InvenTree.tasks.offload_task(InvenTree.tasks.delete_old_error_logs) + + # Check that it is empty again + errors = Error.objects.filter(when__lte=threshold,) + self.assertEqual(len(errors), 0) + + def test_task_check_for_updates(self): + """Test the task check_for_updates""" + # Check that setting should be empty + self.assertEqual(InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION'), '') + + # Get new version + InvenTree.tasks.offload_task(InvenTree.tasks.check_for_updates) + + # Check that setting is not empty + response = InvenTreeSetting.get_setting('INVENTREE_LATEST_VERSION') + self.assertNotEqual(response, '') + self.assertTrue(bool(response)) diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py index d70752ad00..5be5fa3519 100644 --- a/InvenTree/InvenTree/views.py +++ b/InvenTree/InvenTree/views.py @@ -5,8 +5,6 @@ In particular these views provide base functionality for rendering Django forms as JSON objects and passing them to modal forms (using jQuery / bootstrap). """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import os import json @@ -797,13 +795,9 @@ class CurrencyRefreshView(RedirectView): On a POST request we will attempt to refresh the exchange rates """ - from InvenTree.tasks import offload_task + from InvenTree.tasks import offload_task, update_exchange_rates - # Define associated task from InvenTree.tasks list of methods - taskname = 'InvenTree.tasks.update_exchange_rates' - - # Run it - offload_task(taskname, force_sync=True) + offload_task(update_exchange_rates, force_sync=True) return redirect(reverse_lazy('settings')) diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index 43909d2197..32d843d057 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib import admin from import_export.admin import ImportExportModelAdmin diff --git a/InvenTree/build/api.py b/InvenTree/build/api.py index 4453232823..c6c1c73128 100644 --- a/InvenTree/build/api.py +++ b/InvenTree/build/api.py @@ -2,9 +2,6 @@ JSON API for the Build app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.urls import include, re_path from rest_framework import filters, generics @@ -285,6 +282,13 @@ class BuildOutputDelete(BuildOrderContextMixin, generics.CreateAPIView): API endpoint for deleting multiple build outputs """ + def get_serializer_context(self): + ctx = super().get_serializer_context() + + ctx['to_complete'] = False + + return ctx + queryset = Build.objects.none() serializer_class = build.serializers.BuildOutputDeleteSerializer diff --git a/InvenTree/build/apps.py b/InvenTree/build/apps.py index a5b69d365a..9025e77e0e 100644 --- a/InvenTree/build/apps.py +++ b/InvenTree/build/apps.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/InvenTree/build/forms.py b/InvenTree/build/forms.py deleted file mode 100644 index 77a42571d8..0000000000 --- a/InvenTree/build/forms.py +++ /dev/null @@ -1,7 +0,0 @@ -""" -Django Forms for interacting with Build objects -""" - -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals diff --git a/InvenTree/build/models.py b/InvenTree/build/models.py index 0d7f6e3870..0f539dc158 100644 --- a/InvenTree/build/models.py +++ b/InvenTree/build/models.py @@ -2,8 +2,6 @@ Build database model definitions """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import decimal import os @@ -1141,12 +1139,13 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs): """ Callback function to be executed after a Build instance is saved """ + from . import tasks as build_tasks if created: # A new Build has just been created # Run checks on required parts - InvenTree.tasks.offload_task('build.tasks.check_build_stock', instance) + InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance) class BuildOrderAttachment(InvenTreeAttachment): diff --git a/InvenTree/build/serializers.py b/InvenTree/build/serializers.py index 0f1703750c..e4919f1433 100644 --- a/InvenTree/build/serializers.py +++ b/InvenTree/build/serializers.py @@ -2,9 +2,6 @@ JSON serializers for Build API """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import transaction from django.core.exceptions import ValidationError as DjangoValidationError from django.utils.translation import gettext_lazy as _ @@ -202,7 +199,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): def validate_quantity(self, quantity): - if quantity < 0: + if quantity <= 0: raise ValidationError(_("Quantity must be greater than zero")) part = self.get_part() @@ -212,7 +209,7 @@ class BuildOutputCreateSerializer(serializers.Serializer): if part.trackable: raise ValidationError(_("Integer quantity required for trackable parts")) - if part.has_trackable_parts(): + if part.has_trackable_parts: raise ValidationError(_("Integer quantity required, as the bill of materials contains trackable parts")) return quantity @@ -235,7 +232,6 @@ class BuildOutputCreateSerializer(serializers.Serializer): serial_numbers = serial_numbers.strip() - # TODO: Field level validation necessary here? return serial_numbers auto_allocate = serializers.BooleanField( diff --git a/InvenTree/build/tasks.py b/InvenTree/build/tasks.py index b69057d444..daf231d951 100644 --- a/InvenTree/build/tasks.py +++ b/InvenTree/build/tasks.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from decimal import Decimal import logging diff --git a/InvenTree/build/test_api.py b/InvenTree/build/test_api.py index a54a92dda8..068493ad5e 100644 --- a/InvenTree/build/test_api.py +++ b/InvenTree/build/test_api.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from datetime import datetime, timedelta from django.urls import reverse @@ -305,6 +302,215 @@ class BuildTest(BuildAPITest): self.assertEqual(bo.status, BuildStatus.CANCELLED) + def test_create_delete_output(self): + """ + Test that we can create and delete build outputs via the API + """ + + bo = Build.objects.get(pk=1) + + n_outputs = bo.output_count + + create_url = reverse('api-build-output-create', kwargs={'pk': 1}) + + # Attempt to create outputs with invalid data + response = self.post( + create_url, + { + 'quantity': 'not a number', + }, + expected_code=400 + ) + + self.assertIn('A valid number is required', str(response.data)) + + for q in [-100, -10.3, 0]: + + response = self.post( + create_url, + { + 'quantity': q, + }, + expected_code=400 + ) + + if q == 0: + self.assertIn('Quantity must be greater than zero', str(response.data)) + else: + self.assertIn('Ensure this value is greater than or equal to 0', str(response.data)) + + # Mark the part being built as 'trackable' (requires integer quantity) + bo.part.trackable = True + bo.part.save() + + response = self.post( + create_url, + { + 'quantity': 12.3, + }, + expected_code=400 + ) + + self.assertIn('Integer quantity required for trackable parts', str(response.data)) + + # Erroneous serial numbers + response = self.post( + create_url, + { + 'quantity': 5, + 'serial_numbers': '1, 2, 3, 4, 5, 6', + 'batch': 'my-batch', + }, + expected_code=400 + ) + + self.assertIn('Number of unique serial numbers (6) must match quantity (5)', str(response.data)) + + # At this point, no new build outputs should have been created + self.assertEqual(n_outputs, bo.output_count) + + # Now, create with *good* data + response = self.post( + create_url, + { + 'quantity': 5, + 'serial_numbers': '1, 2, 3, 4, 5', + 'batch': 'my-batch', + }, + expected_code=201, + ) + + # 5 new outputs have been created + self.assertEqual(n_outputs + 5, bo.output_count) + + # Attempt to create with identical serial numbers + response = self.post( + create_url, + { + 'quantity': 3, + 'serial_numbers': '1-3', + }, + expected_code=400, + ) + + self.assertIn('The following serial numbers already exist : 1,2,3', str(response.data)) + + # Double check no new outputs have been created + self.assertEqual(n_outputs + 5, bo.output_count) + + # Now, let's delete each build output individually via the API + outputs = bo.build_outputs.all() + + delete_url = reverse('api-build-output-delete', kwargs={'pk': 1}) + + response = self.post( + delete_url, + { + 'outputs': [], + }, + expected_code=400 + ) + + self.assertIn('A list of build outputs must be provided', str(response.data)) + + # Mark 1 build output as complete + bo.complete_build_output(outputs[0], self.user) + + self.assertEqual(n_outputs + 5, bo.output_count) + self.assertEqual(1, bo.complete_count) + + # Delete all outputs at once + # Note: One has been completed, so this should fail! + response = self.post( + delete_url, + { + 'outputs': [ + { + 'output': output.pk, + } for output in outputs + ] + }, + expected_code=400 + ) + + self.assertIn('This build output has already been completed', str(response.data)) + + # No change to the build outputs + self.assertEqual(n_outputs + 5, bo.output_count) + self.assertEqual(1, bo.complete_count) + + # Let's delete 2 build outputs + response = self.post( + delete_url, + { + 'outputs': [ + { + 'output': output.pk, + } for output in outputs[1:3] + ] + }, + expected_code=201 + ) + + # Two build outputs have been removed + self.assertEqual(n_outputs + 3, bo.output_count) + self.assertEqual(1, bo.complete_count) + + # Tests for BuildOutputComplete serializer + complete_url = reverse('api-build-output-complete', kwargs={'pk': 1}) + + # Let's mark the remaining outputs as complete + response = self.post( + complete_url, + { + 'outputs': [], + 'location': 4, + }, + expected_code=400, + ) + + self.assertIn('A list of build outputs must be provided', str(response.data)) + + for output in outputs[3:]: + output.refresh_from_db() + self.assertTrue(output.is_building) + + response = self.post( + complete_url, + { + 'outputs': [ + { + 'output': output.pk + } for output in outputs[3:] + ], + 'location': 4, + }, + expected_code=201, + ) + + # Check that the outputs have been completed + self.assertEqual(3, bo.complete_count) + + for output in outputs[3:]: + output.refresh_from_db() + self.assertEqual(output.location.pk, 4) + self.assertFalse(output.is_building) + + # Try again, with an output which has already been completed + response = self.post( + complete_url, + { + 'outputs': [ + { + 'output': outputs.last().pk, + } + ] + }, + expected_code=400, + ) + + self.assertIn('This build output has already been completed', str(response.data)) + class BuildAllocationTest(BuildAPITest): """ diff --git a/InvenTree/build/tests.py b/InvenTree/build/tests.py index e8ec8b67ca..d3c0c41112 100644 --- a/InvenTree/build/tests.py +++ b/InvenTree/build/tests.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.test import TestCase from django.urls import reverse diff --git a/InvenTree/build/views.py b/InvenTree/build/views.py index c8ac4509b8..57d8f8bd37 100644 --- a/InvenTree/build/views.py +++ b/InvenTree/build/views.py @@ -2,9 +2,6 @@ Django views for interacting with Build objects """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView diff --git a/InvenTree/common/admin.py b/InvenTree/common/admin.py index b49bef1c07..f9ae7b557f 100644 --- a/InvenTree/common/admin.py +++ b/InvenTree/common/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib import admin from import_export.admin import ImportExportModelAdmin diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 1996a4bdbf..646025ab2a 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -2,9 +2,6 @@ Provides a JSON API for common components. """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import json from django.http.response import HttpResponse diff --git a/InvenTree/common/forms.py b/InvenTree/common/forms.py index 4a2a1601aa..7fcdf8535c 100644 --- a/InvenTree/common/forms.py +++ b/InvenTree/common/forms.py @@ -2,9 +2,6 @@ Django forms for interacting with common objects """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django import forms from django.utils.translation import gettext as _ diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 36f20c42a2..83773fe48a 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -3,9 +3,6 @@ Common database model definitions. These models are 'generic' and do not fit a particular business logic object. """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os import decimal import math diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index 8dd0f5bcee..7d0c8cc219 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -2,9 +2,6 @@ JSON serializers for common components """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.helpers import get_objectreference diff --git a/InvenTree/common/settings.py b/InvenTree/common/settings.py index 2dd7756eba..361284d831 100644 --- a/InvenTree/common/settings.py +++ b/InvenTree/common/settings.py @@ -2,9 +2,6 @@ User-configurable settings for the common app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from moneyed import CURRENCIES from django.conf import settings diff --git a/InvenTree/common/tasks.py b/InvenTree/common/tasks.py index c83f0e6bad..db63dc9c17 100644 --- a/InvenTree/common/tasks.py +++ b/InvenTree/common/tasks.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import logging from datetime import timedelta, datetime diff --git a/InvenTree/common/test_notifications.py b/InvenTree/common/test_notifications.py index 719d9291de..e16e28f339 100644 --- a/InvenTree/common/test_notifications.py +++ b/InvenTree/common/test_notifications.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from common.notifications import NotificationMethod, SingleNotificationMethod, BulkNotificationMethod, storage from plugin.models import NotificationUserSetting from part.test_part import BaseNotificationIntegrationTest diff --git a/InvenTree/common/test_tasks.py b/InvenTree/common/test_tasks.py index 3f85316c41..c28a1d19fe 100644 --- a/InvenTree/common/test_tasks.py +++ b/InvenTree/common/test_tasks.py @@ -2,6 +2,7 @@ from django.test import TestCase from common.models import NotificationEntry +from . import tasks as common_tasks from InvenTree.tasks import offload_task @@ -14,4 +15,4 @@ class TaskTest(TestCase): # check empty run self.assertEqual(NotificationEntry.objects.all().count(), 0) - offload_task('common.tasks.delete_old_notifications',) + offload_task(common_tasks.delete_old_notifications,) diff --git a/InvenTree/common/test_views.py b/InvenTree/common/test_views.py index 7d7bfde87e..2394913c73 100644 --- a/InvenTree/common/test_views.py +++ b/InvenTree/common/test_views.py @@ -1,6 +1,3 @@ """ Unit tests for the views associated with the 'common' app """ - -# -*- coding: utf-8 -*- -from __future__ import unicode_literals diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py index 6cf67c6583..8e3f69c21e 100644 --- a/InvenTree/common/tests.py +++ b/InvenTree/common/tests.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals + from http import HTTPStatus import json from datetime import timedelta diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py index 7fae6f3710..f9a6a4f82a 100644 --- a/InvenTree/common/views.py +++ b/InvenTree/common/views.py @@ -2,9 +2,6 @@ Django views for interacting with common models """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os from django.utils.translation import gettext_lazy as _ diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index 97327a559a..cc672f9ee5 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib import admin from import_export.admin import ImportExportModelAdmin diff --git a/InvenTree/company/api.py b/InvenTree/company/api.py index b99fbd01fb..146a45f648 100644 --- a/InvenTree/company/api.py +++ b/InvenTree/company/api.py @@ -2,9 +2,6 @@ Provides a JSON API for the Company app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django_filters.rest_framework import DjangoFilterBackend from django_filters import rest_framework as rest_filters diff --git a/InvenTree/company/apps.py b/InvenTree/company/apps.py index 41371dd739..a0cd4919cf 100644 --- a/InvenTree/company/apps.py +++ b/InvenTree/company/apps.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/InvenTree/company/forms.py b/InvenTree/company/forms.py index 5549f66222..eaead82151 100644 --- a/InvenTree/company/forms.py +++ b/InvenTree/company/forms.py @@ -2,9 +2,6 @@ Django Forms for interacting with Company app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from InvenTree.forms import HelperForm from InvenTree.fields import RoundingDecimalFormField diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py index 5f6f254e2d..40afa6db6d 100644 --- a/InvenTree/company/models.py +++ b/InvenTree/company/models.py @@ -2,9 +2,6 @@ Company database model definitions """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os from django.utils.translation import gettext_lazy as _ diff --git a/InvenTree/company/test_views.py b/InvenTree/company/test_views.py index 258090ebdb..6d28e85d23 100644 --- a/InvenTree/company/test_views.py +++ b/InvenTree/company/test_views.py @@ -1,8 +1,5 @@ """ Unit tests for Company views (see views.py) """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py index 79ffc34af5..edc34e7f2d 100644 --- a/InvenTree/company/tests.py +++ b/InvenTree/company/tests.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.test import TestCase from django.core.exceptions import ValidationError diff --git a/InvenTree/company/views.py b/InvenTree/company/views.py index a9e7054b7a..03514b51b3 100644 --- a/InvenTree/company/views.py +++ b/InvenTree/company/views.py @@ -2,10 +2,6 @@ Django views for interacting with Company app """ - -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView, ListView diff --git a/InvenTree/label/admin.py b/InvenTree/label/admin.py index 8fee2b1f8f..96e90363e0 100644 --- a/InvenTree/label/admin.py +++ b/InvenTree/label/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib import admin from .models import StockItemLabel, StockLocationLabel, PartLabel diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index cb4b939157..17aef828c0 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from io import BytesIO from PIL import Image @@ -24,6 +21,7 @@ from plugin.registry import registry from stock.models import StockItem, StockLocation from part.models import Part +from plugin.base.label import label as plugin_label from .models import StockItemLabel, StockLocationLabel, PartLabel from .serializers import StockItemLabelSerializer, StockLocationLabelSerializer, PartLabelSerializer @@ -156,7 +154,7 @@ class LabelPrintMixin: # Offload a background task to print the provided label offload_task( - 'plugin.base.label.label.print_label', + plugin_label.print_label, plugin.plugin_slug(), image, label_instance=label_instance, diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py index 9094b957ff..7a52ed5d02 100644 --- a/InvenTree/label/models.py +++ b/InvenTree/label/models.py @@ -2,9 +2,6 @@ Label printing models """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import sys import os import logging diff --git a/InvenTree/label/serializers.py b/InvenTree/label/serializers.py index 47ccd51ba1..af6ae49e28 100644 --- a/InvenTree/label/serializers.py +++ b/InvenTree/label/serializers.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField diff --git a/InvenTree/label/test_api.py b/InvenTree/label/test_api.py index a444cd47dc..807de5b63e 100644 --- a/InvenTree/label/test_api.py +++ b/InvenTree/label/test_api.py @@ -1,8 +1,5 @@ # Tests for labels -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.urls import reverse from InvenTree.api_tester import InvenTreeAPITestCase diff --git a/InvenTree/label/tests.py b/InvenTree/label/tests.py index ad1aaba9c8..9e066aa91e 100644 --- a/InvenTree/label/tests.py +++ b/InvenTree/label/tests.py @@ -1,8 +1,5 @@ # Tests for labels -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os from django.conf import settings diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index a1e1b74256..eaeebff04d 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib import admin from import_export.admin import ImportExportModelAdmin diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py index 966a01eb93..e65463d55c 100644 --- a/InvenTree/order/api.py +++ b/InvenTree/order/api.py @@ -2,9 +2,6 @@ JSON API for the Order app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.urls import include, path, re_path from django.db.models import Q, F diff --git a/InvenTree/order/forms.py b/InvenTree/order/forms.py index a08cf81ab1..120d1e0923 100644 --- a/InvenTree/order/forms.py +++ b/InvenTree/order/forms.py @@ -2,9 +2,6 @@ Django Forms for interacting with Order objects """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django import forms from django.utils.translation import gettext_lazy as _ diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py index ede4c806ef..47eacc40a0 100644 --- a/InvenTree/order/serializers.py +++ b/InvenTree/order/serializers.py @@ -2,9 +2,6 @@ JSON serializers for the Order API """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from decimal import Decimal from django.utils.translation import gettext_lazy as _ diff --git a/InvenTree/order/test_views.py b/InvenTree/order/test_views.py index a0445e3dd6..e38ea7ecef 100644 --- a/InvenTree/order/test_views.py +++ b/InvenTree/order/test_views.py @@ -1,8 +1,5 @@ """ Unit tests for Order views (see views.py) """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.test import TestCase from django.urls import reverse from django.contrib.auth import get_user_model diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py index f8102908e9..77b5741c3e 100644 --- a/InvenTree/order/views.py +++ b/InvenTree/order/views.py @@ -2,9 +2,6 @@ Django views for interacting with Order app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db.utils import IntegrityError from django.http.response import JsonResponse from django.shortcuts import get_object_or_404 diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 30dad8e995..8021bd10fa 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib import admin from import_export.admin import ImportExportModelAdmin diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py index 7213f8af4d..de335087f4 100644 --- a/InvenTree/part/api.py +++ b/InvenTree/part/api.py @@ -2,9 +2,6 @@ Provides a JSON API for the Part app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import datetime from django.urls import include, path, re_path diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py index 93885995ac..b1dfcbe8bc 100644 --- a/InvenTree/part/apps.py +++ b/InvenTree/part/apps.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import logging from django.db.utils import OperationalError, ProgrammingError diff --git a/InvenTree/part/forms.py b/InvenTree/part/forms.py index 87210901d2..9a14d8ad25 100644 --- a/InvenTree/part/forms.py +++ b/InvenTree/part/forms.py @@ -2,9 +2,6 @@ Django Forms for interacting with Part objects """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django import forms from django.utils.translation import gettext_lazy as _ diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 64593e9756..fa0f1816f8 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -2,8 +2,6 @@ Part database model definitions """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import decimal import os @@ -61,7 +59,6 @@ from order import models as OrderModels from company.models import SupplierPart import part.settings as part_settings from stock import models as StockModels - from plugin.models import MetadataMixin @@ -2293,12 +2290,13 @@ def after_save_part(sender, instance: Part, created, **kwargs): """ Function to be executed after a Part is saved """ + from part import tasks as part_tasks if not created and not InvenTree.ready.isImportingData(): # Check part stock only if we are *updating* the part (not creating it) # Run this check in the background - InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance) + InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance) class PartAttachment(InvenTreeAttachment): diff --git a/InvenTree/part/settings.py b/InvenTree/part/settings.py index e345a9d88d..41ecbb0d00 100644 --- a/InvenTree/part/settings.py +++ b/InvenTree/part/settings.py @@ -2,9 +2,6 @@ User-configurable settings for the Part app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from common.models import InvenTreeSetting diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index b158d26ad3..7ab15a7200 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import logging from django.utils.translation import gettext_lazy as _ @@ -49,6 +46,6 @@ def notify_low_stock_if_required(part: part.models.Part): for p in parts: if p.is_part_low_on_stock(): InvenTree.tasks.offload_task( - 'part.tasks.notify_low_stock', + notify_low_stock, p ) diff --git a/InvenTree/part/templates/part/detail.html b/InvenTree/part/templates/part/detail.html index bcedc95da8..1c80a88c28 100644 --- a/InvenTree/part/templates/part/detail.html +++ b/InvenTree/part/templates/part/detail.html @@ -589,32 +589,15 @@ // Get a list of the selected BOM items var rows = $("#bom-table").bootstrapTable('getSelections'); - // TODO - In the future, display (in the dialog) which items are going to be deleted + if (rows.length == 0) { + rows = $('#bom-table').bootstrapTable('getData'); + } - showQuestionDialog( - '{% trans "Delete selected BOM items?" %}', - '{% trans "All selected BOM items will be deleted" %}', - { - accept: function() { - - // Keep track of each DELETE request - var requests = []; - - rows.forEach(function(row) { - requests.push( - inventreeDelete( - `/api/bom/${row.pk}/`, - ) - ); - }); - - // Wait for *all* the requests to complete - $.when.apply($, requests).done(function() { - location.reload(); - }); - } + deleteBomItems(rows, { + success: function() { + $('#bom-table').bootstrapTable('refresh'); } - ); + }); }); $('#bom-upload').click(function() { diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py index dc79a3a123..28df4503c9 100644 --- a/InvenTree/part/test_api.py +++ b/InvenTree/part/test_api.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import PIL from django.urls import reverse diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py index 0789ed08c3..1ee57e8d07 100644 --- a/InvenTree/part/test_bom_item.py +++ b/InvenTree/part/test_bom_item.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.db import transaction from django.test import TestCase diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py index fe3514d807..db617f6ffb 100644 --- a/InvenTree/part/test_param.py +++ b/InvenTree/part/test_param.py @@ -1,8 +1,5 @@ # Tests for Part Parameters -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.test import TestCase, TransactionTestCase import django.core.exceptions as django_exceptions diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py index f1bfcab40a..09e8f97c64 100644 --- a/InvenTree/part/test_part.py +++ b/InvenTree/part/test_part.py @@ -1,8 +1,5 @@ # Tests for the Part model -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals from allauth.account.models import EmailAddress from django.contrib.auth import get_user_model diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py index 7b5021fb45..78dcc5b63f 100644 --- a/InvenTree/part/views.py +++ b/InvenTree/part/views.py @@ -2,9 +2,6 @@ Django views for interacting with Part app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.core.files.base import ContentFile from django.core.exceptions import ValidationError from django.db import transaction diff --git a/InvenTree/plugin/admin.py b/InvenTree/plugin/admin.py index 256b5ce0da..dfc8e861f3 100644 --- a/InvenTree/plugin/admin.py +++ b/InvenTree/plugin/admin.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.contrib import admin diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index 50e2b28795..4b0f809001 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -2,9 +2,6 @@ JSON API for the plugin app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.conf import settings from django.urls import include, re_path diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py index 521d42b743..233f037ec7 100644 --- a/InvenTree/plugin/apps.py +++ b/InvenTree/plugin/apps.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import logging diff --git a/InvenTree/plugin/base/event/events.py b/InvenTree/plugin/base/event/events.py index 59dfdce86a..4dea7525b3 100644 --- a/InvenTree/plugin/base/event/events.py +++ b/InvenTree/plugin/base/event/events.py @@ -2,9 +2,6 @@ Functions for triggering and responding to server side events """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import logging from django.conf import settings @@ -40,7 +37,7 @@ def trigger_event(event, *args, **kwargs): logger.debug(f"Event triggered: '{event}'") offload_task( - 'plugin.events.register_event', + register_event, event, *args, **kwargs @@ -75,7 +72,7 @@ def register_event(event, *args, **kwargs): # Offload a separate task for each plugin offload_task( - 'plugin.events.process_event', + process_event, slug, event, *args, diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py index 30c6d749a9..a6776f2d40 100644 --- a/InvenTree/plugin/base/locate/api.py +++ b/InvenTree/plugin/base/locate/api.py @@ -1,8 +1,5 @@ """API for location plugins""" -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from rest_framework import permissions from rest_framework.exceptions import ParseError, NotFound from rest_framework.response import Response @@ -56,7 +53,7 @@ class LocatePluginView(APIView): try: StockItem.objects.get(pk=item_pk) - offload_task('plugin.registry.call_function', plugin, 'locate_stock_item', item_pk) + offload_task(registry.call_function, plugin, 'locate_stock_item', item_pk) data['item'] = item_pk @@ -69,7 +66,7 @@ class LocatePluginView(APIView): try: StockLocation.objects.get(pk=location_pk) - offload_task('plugin.registry.call_function', plugin, 'locate_stock_location', location_pk) + offload_task(registry.call_function, plugin, 'locate_stock_location', location_pk) data['location'] = location_pk diff --git a/InvenTree/plugin/builtin/integration/test_core_notifications.py b/InvenTree/plugin/builtin/integration/test_core_notifications.py index 02c91784e5..e93d2a84cd 100644 --- a/InvenTree/plugin/builtin/integration/test_core_notifications.py +++ b/InvenTree/plugin/builtin/integration/test_core_notifications.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from plugin.models import NotificationUserSetting from part.test_part import BaseNotificationIntegrationTest from plugin.builtin.integration.core_notifications import CoreNotificationsPlugin diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index bea1fafb25..66c6267d53 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -2,8 +2,10 @@ Import helper for events """ -from plugin.base.event.events import trigger_event +from plugin.base.event.events import process_event, register_event, trigger_event __all__ = [ + 'process_event', + 'register_event', 'trigger_event', ] diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index 4db66a30fd..27f77a9eb5 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -2,8 +2,6 @@ Plugin model definitions """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import warnings from django.utils.translation import gettext_lazy as _ diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py index d03b892eb3..b3c57d9291 100644 --- a/InvenTree/plugin/serializers.py +++ b/InvenTree/plugin/serializers.py @@ -2,9 +2,6 @@ JSON serializers for plugin app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os import subprocess diff --git a/InvenTree/plugin/test_api.py b/InvenTree/plugin/test_api.py index 58911a9bd2..e3685969af 100644 --- a/InvenTree/plugin/test_api.py +++ b/InvenTree/plugin/test_api.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.urls import reverse diff --git a/InvenTree/plugin/views.py b/InvenTree/plugin/views.py index 1b45fefbb1..ee855094cc 100644 --- a/InvenTree/plugin/views.py +++ b/InvenTree/plugin/views.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.conf import settings diff --git a/InvenTree/report/admin.py b/InvenTree/report/admin.py index b76b8fab1b..6456bf72c5 100644 --- a/InvenTree/report/admin.py +++ b/InvenTree/report/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib import admin from .models import ReportSnippet, ReportAsset diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py index 8a063001a6..f8b31fef99 100644 --- a/InvenTree/report/api.py +++ b/InvenTree/report/api.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.utils.translation import gettext_lazy as _ from django.urls import include, path, re_path diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py index 5fe98a7094..dcaa2a842d 100644 --- a/InvenTree/report/models.py +++ b/InvenTree/report/models.py @@ -2,9 +2,6 @@ Report template model definitions """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os import sys import logging diff --git a/InvenTree/report/serializers.py b/InvenTree/report/serializers.py index 6e3e36df18..1db0cca1b9 100644 --- a/InvenTree/report/serializers.py +++ b/InvenTree/report/serializers.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from InvenTree.serializers import InvenTreeModelSerializer from InvenTree.serializers import InvenTreeAttachmentSerializerField diff --git a/InvenTree/report/tests.py b/InvenTree/report/tests.py index a40990b527..d30a5814d9 100644 --- a/InvenTree/report/tests.py +++ b/InvenTree/report/tests.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals import os import shutil diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 83de1d2484..3d931e2d7c 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.contrib import admin from import_export.admin import ImportExportModelAdmin diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py index 9f6c3b9191..7bb5d7000b 100644 --- a/InvenTree/stock/api.py +++ b/InvenTree/stock/api.py @@ -2,9 +2,6 @@ JSON API for the Stock app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from collections import OrderedDict from datetime import datetime, timedelta diff --git a/InvenTree/stock/apps.py b/InvenTree/stock/apps.py index de8e50cc26..ca24ee7312 100644 --- a/InvenTree/stock/apps.py +++ b/InvenTree/stock/apps.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals from django.apps import AppConfig diff --git a/InvenTree/stock/forms.py b/InvenTree/stock/forms.py index 7d419ab478..7e615b5eae 100644 --- a/InvenTree/stock/forms.py +++ b/InvenTree/stock/forms.py @@ -2,9 +2,6 @@ Django Forms for interacting with Stock app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from InvenTree.forms import HelperForm from .models import StockItem, StockItemTracking diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index f63f4c9241..6cd0469b75 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -2,10 +2,6 @@ Stock database model definitions """ - -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - import os from jinja2 import Template @@ -2024,10 +2020,11 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs): """ Function to be executed after a StockItem object is deleted """ + from part import tasks as part_tasks if not InvenTree.ready.isImportingData(): # Run this check in the background - InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part) + InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance.part) @receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log') @@ -2035,10 +2032,11 @@ def after_save_stock_item(sender, instance: StockItem, created, **kwargs): """ Hook function to be executed after StockItem object is saved/updated """ + from part import tasks as part_tasks if not InvenTree.ready.isImportingData(): # Run this check in the background - InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part) + InvenTree.tasks.offload_task(part_tasks.notify_low_stock_if_required, instance.part) class StockItemAttachment(InvenTreeAttachment): diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py index 6b27b87f93..40303fc120 100644 --- a/InvenTree/stock/serializers.py +++ b/InvenTree/stock/serializers.py @@ -2,9 +2,6 @@ JSON serializers for Stock app """ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from decimal import Decimal from datetime import datetime, timedelta from django.db import transaction 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/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" %} +
+ + + + + + + + ${rows} +
{% trans "Part" %}{% trans "Reference" %}{% trans "Quantity" %}
+ `; + + 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/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/users/admin.py b/InvenTree/users/admin.py index 9763644479..2784f28efe 100644 --- a/InvenTree/users/admin.py +++ b/InvenTree/users/admin.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from django.utils.translation import gettext_lazy as _ diff --git a/InvenTree/users/api.py b/InvenTree/users/api.py index 47e073fb52..2a78abcbbf 100644 --- a/InvenTree/users/api.py +++ b/InvenTree/users/api.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- - -from __future__ import unicode_literals from django.contrib.auth.models import User from django.core.exceptions import ObjectDoesNotExist diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py index a4668c68e8..b610e23488 100644 --- a/InvenTree/users/apps.py +++ b/InvenTree/users/apps.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db.utils import OperationalError, ProgrammingError from django.apps import AppConfig diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py index e6a4019481..9dc90f5d2c 100644 --- a/InvenTree/users/tests.py +++ b/InvenTree/users/tests.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.test import TestCase from django.apps import apps from django.urls import reverse 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 b071afbe86..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 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():