diff --git a/InvenTree/InvenTree/admin.py b/InvenTree/InvenTree/admin.py
index 6da1d9a2d5..e044e8ae54 100644
--- a/InvenTree/InvenTree/admin.py
+++ b/InvenTree/InvenTree/admin.py
@@ -32,12 +32,12 @@ class InvenTreeResource(ModelResource):
         """Override the default import_data_inner function to provide better error handling"""
         if len(dataset) > self.MAX_IMPORT_ROWS:
             raise ImportExportError(
-                f"Dataset contains too many rows (max {self.MAX_IMPORT_ROWS})"
+                f'Dataset contains too many rows (max {self.MAX_IMPORT_ROWS})'
             )
 
         if len(dataset.headers) > self.MAX_IMPORT_COLS:
             raise ImportExportError(
-                f"Dataset contains too many columns (max {self.MAX_IMPORT_COLS})"
+                f'Dataset contains too many columns (max {self.MAX_IMPORT_COLS})'
             )
 
         return super().import_data_inner(
diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py
index 24f9b58765..3e449398b5 100644
--- a/InvenTree/InvenTree/api.py
+++ b/InvenTree/InvenTree/api.py
@@ -232,19 +232,19 @@ class BulkDeleteMixin:
 
         if not items and not filters:
             raise ValidationError({
-                "non_field_errors": [
-                    "List of items or filters must be provided for bulk deletion"
+                'non_field_errors': [
+                    'List of items or filters must be provided for bulk deletion'
                 ]
             })
 
         if items and type(items) is not list:
             raise ValidationError({
-                "items": ["'items' must be supplied as a list object"]
+                'items': ["'items' must be supplied as a list object"]
             })
 
         if filters and type(filters) is not dict:
             raise ValidationError({
-                "filters": ["'filters' must be supplied as a dict object"]
+                'filters': ["'filters' must be supplied as a dict object"]
             })
 
         # Keep track of how many items we deleted
@@ -266,7 +266,7 @@ class BulkDeleteMixin:
             n_deleted = queryset.count()
             queryset.delete()
 
-        return Response({'success': f"Deleted {n_deleted} items"}, status=204)
+        return Response({'success': f'Deleted {n_deleted} items'}, status=204)
 
 
 class ListCreateDestroyAPIView(BulkDeleteMixin, ListCreateAPI):
@@ -308,7 +308,7 @@ class APIDownloadMixin:
 
     def download_queryset(self, queryset, export_format):
         """This function must be implemented to provide a downloadFile request."""
-        raise NotImplementedError("download_queryset method not implemented!")
+        raise NotImplementedError('download_queryset method not implemented!')
 
 
 class AttachmentMixin:
diff --git a/InvenTree/InvenTree/apps.py b/InvenTree/InvenTree/apps.py
index 1f22d4e98d..87328169c4 100644
--- a/InvenTree/InvenTree/apps.py
+++ b/InvenTree/InvenTree/apps.py
@@ -21,7 +21,7 @@ from InvenTree.ready import (
     isPluginRegistryLoaded,
 )
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 class InvenTreeConfig(AppConfig):
@@ -82,11 +82,11 @@ class InvenTreeConfig(AppConfig):
         try:
             Schedule.objects.filter(func__in=obsolete).delete()
         except Exception:
-            logger.exception("Failed to remove obsolete tasks - database not ready")
+            logger.exception('Failed to remove obsolete tasks - database not ready')
 
     def start_background_tasks(self):
         """Start all background tests for InvenTree."""
-        logger.info("Starting background tasks...")
+        logger.info('Starting background tasks...')
 
         from django_q.models import Schedule
 
@@ -130,17 +130,17 @@ class InvenTreeConfig(AppConfig):
 
         if len(tasks_to_create) > 0:
             Schedule.objects.bulk_create(tasks_to_create)
-            logger.info("Created %s new scheduled tasks", len(tasks_to_create))
+            logger.info('Created %s new scheduled tasks', len(tasks_to_create))
 
         if len(tasks_to_update) > 0:
             Schedule.objects.bulk_update(tasks_to_update, ['schedule_type', 'minutes'])
-            logger.info("Updated %s existing scheduled tasks", len(tasks_to_update))
+            logger.info('Updated %s existing scheduled tasks', len(tasks_to_update))
 
         # Put at least one task onto the background worker stack,
         # which will be processed as soon as the worker comes online
         InvenTree.tasks.offload_task(InvenTree.tasks.heartbeat, force_async=True)
 
-        logger.info("Started %s scheduled background tasks...", len(tasks))
+        logger.info('Started %s scheduled background tasks...', len(tasks))
 
     def collect_tasks(self):
         """Collect all background tasks."""
@@ -152,7 +152,7 @@ class InvenTreeConfig(AppConfig):
                 try:
                     import_module(f'{app.module.__package__}.tasks')
                 except Exception as e:  # pragma: no cover
-                    logger.exception("Error loading tasks for %s: %s", app_name, e)
+                    logger.exception('Error loading tasks for %s: %s', app_name, e)
 
     def update_exchange_rates(self):  # pragma: no cover
         """Update exchange rates each time the server is started.
@@ -183,20 +183,20 @@ class InvenTreeConfig(AppConfig):
 
                 if last_update is None:
                     # Never been updated
-                    logger.info("Exchange backend has never been updated")
+                    logger.info('Exchange backend has never been updated')
                     update = True
 
                 # Backend currency has changed?
                 if base_currency != backend.base_currency:
                     logger.info(
-                        "Base currency changed from %s to %s",
+                        'Base currency changed from %s to %s',
                         backend.base_currency,
                         base_currency,
                     )
                     update = True
 
         except ExchangeBackend.DoesNotExist:
-            logger.info("Exchange backend not found - updating")
+            logger.info('Exchange backend not found - updating')
             update = True
 
         except Exception:
@@ -207,9 +207,9 @@ class InvenTreeConfig(AppConfig):
             try:
                 update_exchange_rates()
             except OperationalError:
-                logger.warning("Could not update exchange rates - database not ready")
+                logger.warning('Could not update exchange rates - database not ready')
             except Exception as e:
-                logger.exception("Error updating exchange rates: %s (%s)", e, type(e))
+                logger.exception('Error updating exchange rates: %s (%s)', e, type(e))
 
     def add_user_on_startup(self):
         """Add a user on startup."""
@@ -222,7 +222,7 @@ class InvenTreeConfig(AppConfig):
         add_email = get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email')
         add_password = get_setting('INVENTREE_ADMIN_PASSWORD', 'admin_password')
         add_password_file = get_setting(
-            "INVENTREE_ADMIN_PASSWORD_FILE", "admin_password_file", None
+            'INVENTREE_ADMIN_PASSWORD_FILE', 'admin_password_file', None
         )
 
         # check if all values are present
@@ -260,7 +260,7 @@ class InvenTreeConfig(AppConfig):
         try:
             with transaction.atomic():
                 if user.objects.filter(username=add_user).exists():
-                    logger.info("User %s already exists - skipping creation", add_user)
+                    logger.info('User %s already exists - skipping creation', add_user)
                 else:
                     new_user = user.objects.create_superuser(
                         add_user, add_email, add_password
@@ -272,12 +272,12 @@ class InvenTreeConfig(AppConfig):
     def add_user_from_file(self):
         """Add the superuser from a file."""
         # stop if checks were already created
-        if hasattr(settings, "USER_ADDED_FILE") and settings.USER_ADDED_FILE:
+        if hasattr(settings, 'USER_ADDED_FILE') and settings.USER_ADDED_FILE:
             return
 
         # get values
         add_password_file = get_setting(
-            "INVENTREE_ADMIN_PASSWORD_FILE", "admin_password_file", None
+            'INVENTREE_ADMIN_PASSWORD_FILE', 'admin_password_file', None
         )
 
         # no variable set -> do not try anything
@@ -296,7 +296,7 @@ class InvenTreeConfig(AppConfig):
         self._create_admin_user(
             get_setting('INVENTREE_ADMIN_USER', 'admin_user', 'admin'),
             get_setting('INVENTREE_ADMIN_EMAIL', 'admin_email', ''),
-            add_password_file.read_text(encoding="utf-8"),
+            add_password_file.read_text(encoding='utf-8'),
         )
 
         # do not try again
diff --git a/InvenTree/InvenTree/ci_render_js.py b/InvenTree/InvenTree/ci_render_js.py
index 72e493c720..6aa3847895 100644
--- a/InvenTree/InvenTree/ci_render_js.py
+++ b/InvenTree/InvenTree/ci_render_js.py
@@ -63,9 +63,9 @@ class RenderJavascriptFiles(InvenTreeTestCase):  # pragma: no cover
         """Look for all javascript files."""
         n = 0
 
-        print("Rendering javascript files...")
+        print('Rendering javascript files...')
 
         n += self.download_files('translated', '/js/i18n')
         n += self.download_files('dynamic', '/js/dynamic')
 
-        print(f"Rendered {n} javascript files.")
+        print(f'Rendered {n} javascript files.')
diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py
index 609ea6e71d..40c849bb36 100644
--- a/InvenTree/InvenTree/config.py
+++ b/InvenTree/InvenTree/config.py
@@ -99,9 +99,9 @@ def get_config_file(create=True) -> Path:
         )
         ensure_dir(cfg_filename.parent)
 
-        cfg_template = base_dir.joinpath("config_template.yaml")
+        cfg_template = base_dir.joinpath('config_template.yaml')
         shutil.copyfile(cfg_template, cfg_filename)
-        print(f"Created config file {cfg_filename}")
+        print(f'Created config file {cfg_filename}')
 
     return cfg_filename
 
@@ -293,14 +293,14 @@ def get_plugin_file():
 
     if not plugin_file.exists():
         logger.warning(
-            "Plugin configuration file does not exist - creating default file"
+            'Plugin configuration file does not exist - creating default file'
         )
         logger.info("Creating plugin file at '%s'", plugin_file)
         ensure_dir(plugin_file.parent)
 
         # If opening the file fails (no write permission, for example), then this will throw an error
         plugin_file.write_text(
-            "# InvenTree Plugins (uses PIP framework to install)\n\n"
+            '# InvenTree Plugins (uses PIP framework to install)\n\n'
         )
 
     return plugin_file
@@ -323,7 +323,7 @@ def get_secret_key():
     """
     # Look for environment variable
     if secret_key := get_setting('INVENTREE_SECRET_KEY', 'secret_key'):
-        logger.info("SECRET_KEY loaded by INVENTREE_SECRET_KEY")  # pragma: no cover
+        logger.info('SECRET_KEY loaded by INVENTREE_SECRET_KEY')  # pragma: no cover
         return secret_key
 
     # Look for secret key file
@@ -331,7 +331,7 @@ def get_secret_key():
         secret_key_file = Path(secret_key_file).resolve()
     else:
         # Default location for secret key file
-        secret_key_file = get_base_dir().joinpath("secret_key.txt").resolve()
+        secret_key_file = get_base_dir().joinpath('secret_key.txt').resolve()
 
     if not secret_key_file.exists():
         logger.info("Generating random key file at '%s'", secret_key_file)
@@ -367,9 +367,9 @@ def get_custom_file(
     static_storage = StaticFilesStorage()
 
     if static_storage.exists(value):
-        logger.info("Loading %s from %s directory: %s", log_ref, 'static', value)
+        logger.info('Loading %s from %s directory: %s', log_ref, 'static', value)
     elif lookup_media and default_storage.exists(value):
-        logger.info("Loading %s from %s directory: %s", log_ref, 'media', value)
+        logger.info('Loading %s from %s directory: %s', log_ref, 'media', value)
     else:
         add_dir_str = ' or media' if lookup_media else ''
         logger.warning(
diff --git a/InvenTree/InvenTree/conversion.py b/InvenTree/InvenTree/conversion.py
index cba34ff460..b87d98c8a5 100644
--- a/InvenTree/InvenTree/conversion.py
+++ b/InvenTree/InvenTree/conversion.py
@@ -127,7 +127,7 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
         if unit:
             raise ValidationError(_(f'Could not convert {original} to {unit}'))
         else:
-            raise ValidationError(_("Invalid quantity supplied"))
+            raise ValidationError(_('Invalid quantity supplied'))
 
     # Calculate the "magnitude" of the value, as a float
     # If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues
diff --git a/InvenTree/InvenTree/email.py b/InvenTree/InvenTree/email.py
index b93c39f63b..a5f7b283df 100644
--- a/InvenTree/InvenTree/email.py
+++ b/InvenTree/InvenTree/email.py
@@ -30,22 +30,22 @@ def is_email_configured():
 
         # Display warning unless in test mode
         if not testing:  # pragma: no cover
-            logger.debug("EMAIL_HOST is not configured")
+            logger.debug('EMAIL_HOST is not configured')
 
     # Display warning unless in test mode
     if not settings.EMAIL_HOST_USER and not testing:  # pragma: no cover
-        logger.debug("EMAIL_HOST_USER is not configured")
+        logger.debug('EMAIL_HOST_USER is not configured')
 
     # Display warning unless in test mode
     if not settings.EMAIL_HOST_PASSWORD and testing:  # pragma: no cover
-        logger.debug("EMAIL_HOST_PASSWORD is not configured")
+        logger.debug('EMAIL_HOST_PASSWORD is not configured')
 
     # Email sender must be configured
     if not settings.DEFAULT_FROM_EMAIL:
         configured = False
 
         if not testing:  # pragma: no cover
-            logger.debug("DEFAULT_FROM_EMAIL is not configured")
+            logger.debug('DEFAULT_FROM_EMAIL is not configured')
 
     return configured
 
@@ -75,7 +75,7 @@ def send_email(subject, body, recipients, from_email=None, html_message=None):
             if settings.TESTING:
                 from_email = 'from@test.com'
             else:
-                logger.error("send_email failed: DEFAULT_FROM_EMAIL not specified")
+                logger.error('send_email failed: DEFAULT_FROM_EMAIL not specified')
                 return
 
     InvenTree.tasks.offload_task(
diff --git a/InvenTree/InvenTree/exceptions.py b/InvenTree/InvenTree/exceptions.py
index 75cbaff125..b2f014390d 100644
--- a/InvenTree/InvenTree/exceptions.py
+++ b/InvenTree/InvenTree/exceptions.py
@@ -86,7 +86,7 @@ def exception_handler(exc, context):
             # If in DEBUG mode, provide error information in the response
             error_detail = str(exc)
         else:
-            error_detail = _("Error details can be found in the admin panel")
+            error_detail = _('Error details can be found in the admin panel')
 
         response_data = {
             'error': type(exc).__name__,
diff --git a/InvenTree/InvenTree/exchange.py b/InvenTree/InvenTree/exchange.py
index f9db24e5d8..9a0d7ecf12 100644
--- a/InvenTree/InvenTree/exchange.py
+++ b/InvenTree/InvenTree/exchange.py
@@ -18,7 +18,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
     Uses the plugin system to actually fetch the rates from an external API.
     """
 
-    name = "InvenTreeExchange"
+    name = 'InvenTreeExchange'
 
     def get_rates(self, **kwargs) -> None:
         """Set the requested currency codes and get rates."""
@@ -55,19 +55,19 @@ class InvenTreeExchange(SimpleExchangeBackend):
         try:
             rates = plugin.update_exchange_rates(base_currency, symbols)
         except Exception as exc:
-            logger.exception("Exchange rate update failed: %s", exc)
+            logger.exception('Exchange rate update failed: %s', exc)
             return {}
 
         if not rates:
             logger.warning(
-                "Exchange rate update failed - no data returned from plugin %s", slug
+                'Exchange rate update failed - no data returned from plugin %s', slug
             )
             return {}
 
         # Update exchange rates based on returned data
         if type(rates) is not dict:
             logger.warning(
-                "Invalid exchange rate data returned from plugin %s (type %s)",
+                'Invalid exchange rate data returned from plugin %s (type %s)',
                 slug,
                 type(rates),
             )
@@ -82,7 +82,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
     def update_rates(self, base_currency=None, **kwargs):
         """Call to update all exchange rates"""
         backend, _ = ExchangeBackend.objects.update_or_create(
-            name=self.name, defaults={"base_currency": base_currency}
+            name=self.name, defaults={'base_currency': base_currency}
         )
 
         if base_currency is None:
@@ -91,7 +91,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
         symbols = currency_codes()
 
         logger.info(
-            "Updating exchange rates for %s (%s currencies)",
+            'Updating exchange rates for %s (%s currencies)',
             base_currency,
             len(symbols),
         )
@@ -110,7 +110,7 @@ class InvenTreeExchange(SimpleExchangeBackend):
             ])
         else:
             logger.info(
-                "No exchange rates returned from backend - currencies not updated"
+                'No exchange rates returned from backend - currencies not updated'
             )
 
-        logger.info("Updated exchange rates for %s", base_currency)
+        logger.info('Updated exchange rates for %s', base_currency)
diff --git a/InvenTree/InvenTree/filters.py b/InvenTree/InvenTree/filters.py
index 38287a5667..a3866cd0a0 100644
--- a/InvenTree/InvenTree/filters.py
+++ b/InvenTree/InvenTree/filters.py
@@ -76,7 +76,7 @@ class InvenTreeSearchFilter(filters.SearchFilter):
 
                 if whole:
                     # Wrap the search term to enable word-boundary matching
-                    term = r"\y" + term + r"\y"
+                    term = r'\y' + term + r'\y'
 
                 terms.append(term)
 
diff --git a/InvenTree/InvenTree/format.py b/InvenTree/InvenTree/format.py
index b2cd45a23d..aa733cd094 100644
--- a/InvenTree/InvenTree/format.py
+++ b/InvenTree/InvenTree/format.py
@@ -64,7 +64,7 @@ def construct_format_regex(fmt_string: str) -> str:
     Raises:
         ValueError: Format string is invalid
     """
-    pattern = "^"
+    pattern = '^'
 
     for group in string.Formatter().parse(fmt_string):
         prefix = group[0]  # Prefix (literal text appearing before this group)
@@ -87,7 +87,7 @@ def construct_format_regex(fmt_string: str) -> str:
             ':',
             ';',
             '|',
-            '\'',
+            "'",
             '"',
         ]
 
@@ -115,9 +115,9 @@ def construct_format_regex(fmt_string: str) -> str:
             # TODO: Introspect required width
             w = '+'
 
-            pattern += f"(?P<{name}>{chr}{w})"
+            pattern += f'(?P<{name}>{chr}{w})'
 
-    pattern += "$"
+    pattern += '$'
 
     return pattern
 
@@ -172,7 +172,7 @@ def extract_named_group(name: str, value: str, fmt_string: str) -> str:
 
     if not result:
         raise ValueError(
-            _("Provided value does not match required pattern: ") + fmt_string
+            _('Provided value does not match required pattern: ') + fmt_string
         )
 
     # And return the value we are interested in
@@ -198,7 +198,7 @@ def format_money(money: Money, decimal_places: int = None, format: str = None) -
     if format:
         pattern = parse_pattern(format)
     else:
-        pattern = locale.currency_formats["standard"]
+        pattern = locale.currency_formats['standard']
         if decimal_places is not None:
             pattern.frac_prec = (decimal_places, decimal_places)
 
diff --git a/InvenTree/InvenTree/forms.py b/InvenTree/InvenTree/forms.py
index 9ae110eea3..c33eaa8fae 100644
--- a/InvenTree/InvenTree/forms.py
+++ b/InvenTree/InvenTree/forms.py
@@ -140,7 +140,7 @@ class SetPasswordForm(HelperForm):
     )
 
     old_password = forms.CharField(
-        label=_("Old password"),
+        label=_('Old password'),
         strip=False,
         required=False,
         widget=forms.PasswordInput(
@@ -178,23 +178,23 @@ class CustomSignupForm(SignupForm):
 
         # check for two mail fields
         if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
-            self.fields["email2"] = forms.EmailField(
-                label=_("Email (again)"),
+            self.fields['email2'] = forms.EmailField(
+                label=_('Email (again)'),
                 widget=forms.TextInput(
                     attrs={
-                        "type": "email",
-                        "placeholder": _("Email address confirmation"),
+                        'type': 'email',
+                        'placeholder': _('Email address confirmation'),
                     }
                 ),
             )
 
         # check for two password fields
         if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'):
-            self.fields.pop("password2")
+            self.fields.pop('password2')
 
         # reorder fields
         set_form_field_order(
-            self, ["username", "email", "email2", "password1", "password2"]
+            self, ['username', 'email', 'email2', 'password1', 'password2']
         )
 
     def clean(self):
@@ -203,10 +203,10 @@ class CustomSignupForm(SignupForm):
 
         # check for two mail fields
         if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
-            email = cleaned_data.get("email")
-            email2 = cleaned_data.get("email2")
+            email = cleaned_data.get('email')
+            email2 = cleaned_data.get('email2')
             if (email and email2) and email != email2:
-                self.add_error("email2", _("You must type the same email each time."))
+                self.add_error('email2', _('You must type the same email each time.'))
 
         return cleaned_data
 
@@ -221,7 +221,7 @@ def registration_enabled():
             return True
         else:
             logger.error(
-                "Registration cannot be enabled, because EMAIL_HOST is not configured."
+                'Registration cannot be enabled, because EMAIL_HOST is not configured.'
             )
     return False
 
@@ -292,7 +292,7 @@ class CustomUrlMixin:
 
     def get_email_confirmation_url(self, request, emailconfirmation):
         """Custom email confirmation (activation) url."""
-        url = reverse("account_confirm_email", args=[emailconfirmation.key])
+        url = reverse('account_confirm_email', args=[emailconfirmation.key])
         return Site.objects.get_current().domain + url
 
 
diff --git a/InvenTree/InvenTree/helpers.py b/InvenTree/InvenTree/helpers.py
index 6b332912bf..3e8b575f41 100644
--- a/InvenTree/InvenTree/helpers.py
+++ b/InvenTree/InvenTree/helpers.py
@@ -36,7 +36,7 @@ def generateTestKey(test_name):
     Tests must be named such that they will have unique keys.
     """
     key = test_name.strip().lower()
-    key = key.replace(" ", "")
+    key = key.replace(' ', '')
 
     # Remove any characters that cannot be used to represent a variable
     key = re.sub(r'[^a-zA-Z0-9]', '', key)
@@ -56,7 +56,7 @@ def constructPathString(path, max_chars=250):
     # Replace middle elements to limit the pathstring
     if len(pathstring) > max_chars:
         n = int(max_chars / 2 - 2)
-        pathstring = pathstring[:n] + "..." + pathstring[-n:]
+        pathstring = pathstring[:n] + '...' + pathstring[-n:]
 
     return pathstring
 
@@ -82,12 +82,12 @@ def TestIfImage(img):
 
 def getBlankImage():
     """Return the qualified path for the 'blank image' placeholder."""
-    return getStaticUrl("img/blank_image.png")
+    return getStaticUrl('img/blank_image.png')
 
 
 def getBlankThumbnail():
     """Return the qualified path for the 'blank image' thumbnail placeholder."""
-    return getStaticUrl("img/blank_image.thumbnail.png")
+    return getStaticUrl('img/blank_image.thumbnail.png')
 
 
 def getLogoImage(as_file=False, custom=True):
@@ -105,13 +105,13 @@ def getLogoImage(as_file=False, custom=True):
 
         if storage is not None:
             if as_file:
-                return f"file://{storage.path(settings.CUSTOM_LOGO)}"
+                return f'file://{storage.path(settings.CUSTOM_LOGO)}'
             return storage.url(settings.CUSTOM_LOGO)
 
     # If we have got to this point, return the default logo
     if as_file:
         path = settings.STATIC_ROOT.joinpath('img/inventree.png')
-        return f"file://{path}"
+        return f'file://{path}'
     return getStaticUrl('img/inventree.png')
 
 
@@ -124,7 +124,7 @@ def getSplashScreen(custom=True):
             return static_storage.url(settings.CUSTOM_SPLASH)
 
     # No custom splash screen
-    return static_storage.url("img/inventree_splash.jpg")
+    return static_storage.url('img/inventree_splash.jpg')
 
 
 def TestIfImageURL(url):
@@ -234,7 +234,7 @@ def increment(value):
         # Provide a default value if provided with a null input
         return '1'
 
-    pattern = r"(.*?)(\d+)?$"
+    pattern = r'(.*?)(\d+)?$'
 
     result = re.search(pattern, value)
 
@@ -293,7 +293,7 @@ def decimal2string(d):
     if '.' not in s:
         return s
 
-    return s.rstrip("0").rstrip(".")
+    return s.rstrip('0').rstrip('.')
 
 
 def decimal2money(d, currency=None):
@@ -395,7 +395,7 @@ def DownloadFile(
         length = len(bytes(data, response.charset))
     response['Content-Length'] = length
 
-    disposition = "inline" if inline else "attachment"
+    disposition = 'inline' if inline else 'attachment'
 
     response['Content-Disposition'] = f'{disposition}; filename={filename}'
 
@@ -455,7 +455,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
     try:
         expected_quantity = int(expected_quantity)
     except ValueError:
-        raise ValidationError([_("Invalid quantity provided")])
+        raise ValidationError([_('Invalid quantity provided')])
 
     if input_string:
         input_string = str(input_string).strip()
@@ -463,7 +463,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
         input_string = ''
 
     if len(input_string) == 0:
-        raise ValidationError([_("Empty serial number string")])
+        raise ValidationError([_('Empty serial number string')])
 
     next_value = increment_serial_number(starting_value)
 
@@ -473,7 +473,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
         next_value = increment_serial_number(next_value)
 
     # Split input string by whitespace or comma (,) characters
-    groups = re.split(r"[\s,]+", input_string)
+    groups = re.split(r'[\s,]+', input_string)
 
     serials = []
     errors = []
@@ -493,7 +493,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
             return
 
         if serial in serials:
-            add_error(_("Duplicate serial") + f": {serial}")
+            add_error(_('Duplicate serial') + f': {serial}')
         else:
             serials.append(serial)
 
@@ -525,7 +525,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
 
                 if a == b:
                     # Invalid group
-                    add_error(_(f"Invalid group range: {group}"))
+                    add_error(_(f'Invalid group range: {group}'))
                     continue
 
                 group_items = []
@@ -556,7 +556,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
                 if len(group_items) > remaining:
                     add_error(
                         _(
-                            f"Group range {group} exceeds allowed quantity ({expected_quantity})"
+                            f'Group range {group} exceeds allowed quantity ({expected_quantity})'
                         )
                     )
                 elif (
@@ -568,7 +568,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
                     for item in group_items:
                         add_serial(item)
                 else:
-                    add_error(_(f"Invalid group range: {group}"))
+                    add_error(_(f'Invalid group range: {group}'))
 
             else:
                 # In the case of a different number of hyphens, simply add the entire group
@@ -586,14 +586,14 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
             sequence_count = max(0, expected_quantity - len(serials))
 
             if len(items) > 2 or len(items) == 0:
-                add_error(_(f"Invalid group sequence: {group}"))
+                add_error(_(f'Invalid group sequence: {group}'))
                 continue
             elif len(items) == 2:
                 try:
                     if items[1]:
                         sequence_count = int(items[1]) + 1
                 except ValueError:
-                    add_error(_(f"Invalid group sequence: {group}"))
+                    add_error(_(f'Invalid group sequence: {group}'))
                     continue
 
             value = items[0]
@@ -612,7 +612,7 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
                 for item in sequence_items:
                     add_serial(item)
             else:
-                add_error(_(f"Invalid group sequence: {group}"))
+                add_error(_(f'Invalid group sequence: {group}'))
 
         else:
             # At this point, we assume that the 'group' is just a single serial value
@@ -622,12 +622,12 @@ def extract_serial_numbers(input_string, expected_quantity: int, starting_value=
         raise ValidationError(errors)
 
     if len(serials) == 0:
-        raise ValidationError([_("No serial numbers found")])
+        raise ValidationError([_('No serial numbers found')])
 
     if len(errors) == 0 and len(serials) != expected_quantity:
         raise ValidationError([
             _(
-                f"Number of unique serial numbers ({len(serials)}) must match quantity ({expected_quantity})"
+                f'Number of unique serial numbers ({len(serials)}) must match quantity ({expected_quantity})'
             )
         ])
 
@@ -666,7 +666,7 @@ def validateFilterString(value, model=None):
         pair = group.split('=')
 
         if len(pair) != 2:
-            raise ValidationError(f"Invalid group: {group}")
+            raise ValidationError(f'Invalid group: {group}')
 
         k, v = pair
 
@@ -674,7 +674,7 @@ def validateFilterString(value, model=None):
         v = v.strip()
 
         if not k or not v:
-            raise ValidationError(f"Invalid group: {group}")
+            raise ValidationError(f'Invalid group: {group}')
 
         results[k] = v
 
@@ -745,7 +745,7 @@ def strip_html_tags(value: str, raise_error=True, field_name=None):
     if len(cleaned) != len(value) and raise_error:
         field = field_name or 'non_field_errors'
 
-        raise ValidationError({field: [_("Remove HTML tags from this value")]})
+        raise ValidationError({field: [_('Remove HTML tags from this value')]})
 
     return cleaned
 
diff --git a/InvenTree/InvenTree/helpers_model.py b/InvenTree/InvenTree/helpers_model.py
index 09e4481ca9..ab76bf00db 100644
--- a/InvenTree/InvenTree/helpers_model.py
+++ b/InvenTree/InvenTree/helpers_model.py
@@ -120,7 +120,7 @@ def download_image_from_url(remote_url, timeout=2.5):
         'INVENTREE_DOWNLOAD_FROM_URL_USER_AGENT'
     )
     if user_agent:
-        headers = {"User-Agent": user_agent}
+        headers = {'User-Agent': user_agent}
     else:
         headers = None
 
@@ -135,28 +135,28 @@ def download_image_from_url(remote_url, timeout=2.5):
         # Throw an error if anything goes wrong
         response.raise_for_status()
     except requests.exceptions.ConnectionError as exc:
-        raise Exception(_("Connection error") + f": {str(exc)}")
+        raise Exception(_('Connection error') + f': {str(exc)}')
     except requests.exceptions.Timeout as exc:
         raise exc
     except requests.exceptions.HTTPError:
         raise requests.exceptions.HTTPError(
-            _("Server responded with invalid status code") + f": {response.status_code}"
+            _('Server responded with invalid status code') + f': {response.status_code}'
         )
     except Exception as exc:
-        raise Exception(_("Exception occurred") + f": {str(exc)}")
+        raise Exception(_('Exception occurred') + f': {str(exc)}')
 
     if response.status_code != 200:
         raise Exception(
-            _("Server responded with invalid status code") + f": {response.status_code}"
+            _('Server responded with invalid status code') + f': {response.status_code}'
         )
 
     try:
         content_length = int(response.headers.get('Content-Length', 0))
     except ValueError:
-        raise ValueError(_("Server responded with invalid Content-Length value"))
+        raise ValueError(_('Server responded with invalid Content-Length value'))
 
     if content_length > max_size:
-        raise ValueError(_("Image size is too large"))
+        raise ValueError(_('Image size is too large'))
 
     # Download the file, ensuring we do not exceed the reported size
     file = io.BytesIO()
@@ -168,12 +168,12 @@ def download_image_from_url(remote_url, timeout=2.5):
         dl_size += len(chunk)
 
         if dl_size > max_size:
-            raise ValueError(_("Image download exceeded maximum size"))
+            raise ValueError(_('Image download exceeded maximum size'))
 
         file.write(chunk)
 
     if dl_size == 0:
-        raise ValueError(_("Remote server returned empty response"))
+        raise ValueError(_('Remote server returned empty response'))
 
     # Now, attempt to convert the downloaded data to a valid image file
     # img.verify() will throw an exception if the image is not valid
@@ -181,7 +181,7 @@ def download_image_from_url(remote_url, timeout=2.5):
         img = Image.open(file).convert()
         img.verify()
     except Exception:
-        raise TypeError(_("Supplied URL is not a valid image file"))
+        raise TypeError(_('Supplied URL is not a valid image file'))
 
     return img
 
diff --git a/InvenTree/InvenTree/magic_login.py b/InvenTree/InvenTree/magic_login.py
index 2456da92b6..725b913bbf 100644
--- a/InvenTree/InvenTree/magic_login.py
+++ b/InvenTree/InvenTree/magic_login.py
@@ -18,13 +18,13 @@ def send_simple_login_email(user, link):
     """Send an email with the login link to this user."""
     site = Site.objects.get_current()
 
-    context = {"username": user.username, "site_name": site.name, "link": link}
+    context = {'username': user.username, 'site_name': site.name, 'link': link}
     email_plaintext_message = render_to_string(
-        "InvenTree/user_simple_login.txt", context
+        'InvenTree/user_simple_login.txt', context
     )
 
     send_mail(
-        _(f"[{site.name}] Log in to the app"),
+        _(f'[{site.name}] Log in to the app'),
         email_plaintext_message,
         settings.DEFAULT_FROM_EMAIL,
         [user.email],
@@ -34,7 +34,7 @@ def send_simple_login_email(user, link):
 class GetSimpleLoginSerializer(serializers.Serializer):
     """Serializer for the simple login view."""
 
-    email = serializers.CharField(label=_("Email"))
+    email = serializers.CharField(label=_('Email'))
 
 
 class GetSimpleLoginView(APIView):
@@ -47,14 +47,14 @@ class GetSimpleLoginView(APIView):
         """Get the token for the current user or fail."""
         serializer = self.serializer_class(data=request.data)
         serializer.is_valid(raise_exception=True)
-        self.email_submitted(email=serializer.data["email"])
-        return Response({"status": "ok"})
+        self.email_submitted(email=serializer.data['email'])
+        return Response({'status': 'ok'})
 
     def email_submitted(self, email):
         """Notify user about link."""
         user = self.get_user(email)
         if user is None:
-            print("user not found:", email)
+            print('user not found:', email)
             return
         link = self.create_link(user)
         send_simple_login_email(user, link)
@@ -68,7 +68,7 @@ class GetSimpleLoginView(APIView):
 
     def create_link(self, user):
         """Create a login link for this user."""
-        link = reverse("sesame-login")
+        link = reverse('sesame-login')
         link = self.request.build_absolute_uri(link)
         link += sesame.utils.get_query_string(user)
         return link
diff --git a/InvenTree/InvenTree/management/commands/clean_settings.py b/InvenTree/InvenTree/management/commands/clean_settings.py
index 4ef8269f24..45f99e98ab 100644
--- a/InvenTree/InvenTree/management/commands/clean_settings.py
+++ b/InvenTree/InvenTree/management/commands/clean_settings.py
@@ -12,7 +12,7 @@ class Command(BaseCommand):
 
     def handle(self, *args, **kwargs):
         """Cleanup old (undefined) settings in the database."""
-        logger.info("Collecting settings")
+        logger.info('Collecting settings')
         from common.models import InvenTreeSetting, InvenTreeUserSetting
 
         # general settings
@@ -35,4 +35,4 @@ class Command(BaseCommand):
                 setting.delete()
                 logger.info("deleted user setting '%s'", setting.key)
 
-        logger.info("checked all settings")
+        logger.info('checked all settings')
diff --git a/InvenTree/InvenTree/management/commands/prerender.py b/InvenTree/InvenTree/management/commands/prerender.py
index 09c0c3b565..466b6666c7 100644
--- a/InvenTree/InvenTree/management/commands/prerender.py
+++ b/InvenTree/InvenTree/management/commands/prerender.py
@@ -58,10 +58,10 @@ class Command(BaseCommand):
         for file in os.listdir(SOURCE_DIR):
             path = os.path.join(SOURCE_DIR, file)
             if os.path.exists(path) and os.path.isfile(path):
-                print(f"render {file}")
+                print(f'render {file}')
                 render_file(file, SOURCE_DIR, TARGET_DIR, locales, ctx)
             else:
                 raise NotImplementedError(
                     'Using multi-level directories is not implemented at this point'
                 )  # TODO multilevel dir if needed
-        print(f"rendered all files in {SOURCE_DIR}")
+        print(f'rendered all files in {SOURCE_DIR}')
diff --git a/InvenTree/InvenTree/management/commands/rebuild_models.py b/InvenTree/InvenTree/management/commands/rebuild_models.py
index b735c43735..02af71f3a5 100644
--- a/InvenTree/InvenTree/management/commands/rebuild_models.py
+++ b/InvenTree/InvenTree/management/commands/rebuild_models.py
@@ -13,50 +13,50 @@ class Command(BaseCommand):
         """Rebuild all database models which leverage the MPTT structure."""
         # Part model
         try:
-            print("Rebuilding Part objects")
+            print('Rebuilding Part objects')
 
             from part.models import Part
 
             Part.objects.rebuild()
         except Exception:
-            print("Error rebuilding Part objects")
+            print('Error rebuilding Part objects')
 
         # Part category
         try:
-            print("Rebuilding PartCategory objects")
+            print('Rebuilding PartCategory objects')
 
             from part.models import PartCategory
 
             PartCategory.objects.rebuild()
         except Exception:
-            print("Error rebuilding PartCategory objects")
+            print('Error rebuilding PartCategory objects')
 
         # StockItem model
         try:
-            print("Rebuilding StockItem objects")
+            print('Rebuilding StockItem objects')
 
             from stock.models import StockItem
 
             StockItem.objects.rebuild()
         except Exception:
-            print("Error rebuilding StockItem objects")
+            print('Error rebuilding StockItem objects')
 
         # StockLocation model
         try:
-            print("Rebuilding StockLocation objects")
+            print('Rebuilding StockLocation objects')
 
             from stock.models import StockLocation
 
             StockLocation.objects.rebuild()
         except Exception:
-            print("Error rebuilding StockLocation objects")
+            print('Error rebuilding StockLocation objects')
 
         # Build model
         try:
-            print("Rebuilding Build objects")
+            print('Rebuilding Build objects')
 
             from build.models import Build
 
             Build.objects.rebuild()
         except Exception:
-            print("Error rebuilding Build objects")
+            print('Error rebuilding Build objects')
diff --git a/InvenTree/InvenTree/management/commands/rebuild_thumbnails.py b/InvenTree/InvenTree/management/commands/rebuild_thumbnails.py
index 102a9e1bf1..8b54c98cc7 100644
--- a/InvenTree/InvenTree/management/commands/rebuild_thumbnails.py
+++ b/InvenTree/InvenTree/management/commands/rebuild_thumbnails.py
@@ -37,20 +37,20 @@ class Command(BaseCommand):
 
     def handle(self, *args, **kwargs):
         """Rebuild all thumbnail images."""
-        logger.info("Rebuilding Part thumbnails")
+        logger.info('Rebuilding Part thumbnails')
 
         for part in Part.objects.exclude(image=None):
             try:
                 self.rebuild_thumbnail(part)
             except (OperationalError, ProgrammingError):
-                logger.exception("ERROR: Database read error.")
+                logger.exception('ERROR: Database read error.')
                 break
 
-        logger.info("Rebuilding Company thumbnails")
+        logger.info('Rebuilding Company thumbnails')
 
         for company in Company.objects.exclude(image=None):
             try:
                 self.rebuild_thumbnail(company)
             except (OperationalError, ProgrammingError):
-                logger.exception("ERROR: abase read error.")
+                logger.exception('ERROR: abase read error.')
                 break
diff --git a/InvenTree/InvenTree/management/commands/wait_for_db.py b/InvenTree/InvenTree/management/commands/wait_for_db.py
index 16223d4f77..6bfdc98b58 100644
--- a/InvenTree/InvenTree/management/commands/wait_for_db.py
+++ b/InvenTree/InvenTree/management/commands/wait_for_db.py
@@ -12,7 +12,7 @@ class Command(BaseCommand):
 
     def handle(self, *args, **kwargs):
         """Wait till the database is ready."""
-        self.stdout.write("Waiting for database...")
+        self.stdout.write('Waiting for database...')
 
         connected = False
 
@@ -25,12 +25,12 @@ class Command(BaseCommand):
                 connected = True
 
             except OperationalError as e:
-                self.stdout.write(f"Could not connect to database: {e}")
+                self.stdout.write(f'Could not connect to database: {e}')
             except ImproperlyConfigured as e:
-                self.stdout.write(f"Improperly configured: {e}")
+                self.stdout.write(f'Improperly configured: {e}')
             else:
                 if not connection.is_usable():
-                    self.stdout.write("Database configuration is not usable")
+                    self.stdout.write('Database configuration is not usable')
 
             if connected:
-                self.stdout.write("Database connection successful!")
+                self.stdout.write('Database connection successful!')
diff --git a/InvenTree/InvenTree/metadata.py b/InvenTree/InvenTree/metadata.py
index 3bde8f84e5..11a61b3a69 100644
--- a/InvenTree/InvenTree/metadata.py
+++ b/InvenTree/InvenTree/metadata.py
@@ -69,7 +69,7 @@ class InvenTreeMetadata(SimpleMetadata):
 
             metadata['model'] = tbl_label
 
-            table = f"{app_label}_{tbl_label}"
+            table = f'{app_label}_{tbl_label}'
 
             actions = metadata.get('actions', None)
 
@@ -87,7 +87,7 @@ class InvenTreeMetadata(SimpleMetadata):
             }
 
             # let the view define a custom rolemap
-            if hasattr(view, "rolemap"):
+            if hasattr(view, 'rolemap'):
                 rolemap.update(view.rolemap)
 
             # Remove any HTTP methods that the user does not have permission for
@@ -264,7 +264,7 @@ class InvenTreeMetadata(SimpleMetadata):
                 model = field.queryset.model
             else:
                 logger.debug(
-                    "Could not extract model for:", field_info.get('label'), '->', field
+                    'Could not extract model for:', field_info.get('label'), '->', field
                 )
                 model = None
 
@@ -286,4 +286,4 @@ class InvenTreeMetadata(SimpleMetadata):
         return field_info
 
 
-InvenTreeMetadata.label_lookup[DependentField] = "dependent field"
+InvenTreeMetadata.label_lookup[DependentField] = 'dependent field'
diff --git a/InvenTree/InvenTree/middleware.py b/InvenTree/InvenTree/middleware.py
index 50a632c6ec..52428cf3ee 100644
--- a/InvenTree/InvenTree/middleware.py
+++ b/InvenTree/InvenTree/middleware.py
@@ -15,7 +15,7 @@ from error_report.middleware import ExceptionProcessor
 from InvenTree.urls import frontendpatterns
 from users.models import ApiToken
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 class AuthRequiredMiddleware(object):
@@ -91,7 +91,7 @@ class AuthRequiredMiddleware(object):
                             authorized = True
 
                     except ApiToken.DoesNotExist:
-                        logger.warning("Access denied for unknown token %s", token_key)
+                        logger.warning('Access denied for unknown token %s', token_key)
 
             # No authorization was found for the request
             if not authorized:
diff --git a/InvenTree/InvenTree/models.py b/InvenTree/InvenTree/models.py
index ef6ef5b4dd..65ca835ce6 100644
--- a/InvenTree/InvenTree/models.py
+++ b/InvenTree/InvenTree/models.py
@@ -303,7 +303,7 @@ class ReferenceIndexingMixin(models.Model):
                 if recent:
                     reference = recent.reference
                 else:
-                    reference = ""
+                    reference = ''
 
         return reference
 
@@ -316,20 +316,20 @@ class ReferenceIndexingMixin(models.Model):
             info = InvenTree.format.parse_format_string(pattern)
         except Exception as exc:
             raise ValidationError({
-                "value": _("Improperly formatted pattern") + ": " + str(exc)
+                'value': _('Improperly formatted pattern') + ': ' + str(exc)
             })
 
         # Check that only 'allowed' keys are provided
         for key in info.keys():
             if key not in ctx.keys():
                 raise ValidationError({
-                    "value": _("Unknown format key specified") + f": '{key}'"
+                    'value': _('Unknown format key specified') + f": '{key}'"
                 })
 
         # Check that the 'ref' variable is specified
         if 'ref' not in info.keys():
             raise ValidationError({
-                'value': _("Missing required format key") + ": 'ref'"
+                'value': _('Missing required format key') + ": 'ref'"
             })
 
     @classmethod
@@ -340,7 +340,7 @@ class ReferenceIndexingMixin(models.Model):
         value = str(value).strip()
 
         if len(value) == 0:
-            raise ValidationError(_("Reference field cannot be empty"))
+            raise ValidationError(_('Reference field cannot be empty'))
 
         # An 'empty' pattern means no further validation is required
         if not pattern:
@@ -348,7 +348,7 @@ class ReferenceIndexingMixin(models.Model):
 
         if not InvenTree.format.validate_string(value, pattern):
             raise ValidationError(
-                _("Reference must match required pattern") + ": " + pattern
+                _('Reference must match required pattern') + ': ' + pattern
             )
 
         # Check that the reference field can be rebuild
@@ -380,7 +380,7 @@ class ReferenceIndexingMixin(models.Model):
 
         if validate:
             if reference_int > models.BigIntegerField.MAX_BIGINT:
-                raise ValidationError({"reference": _("Reference number is too large")})
+                raise ValidationError({'reference': _('Reference number is too large')})
 
         return reference_int
 
@@ -399,7 +399,7 @@ def extract_int(reference, clip=0x7FFFFFFF, allow_negative=False):
         return 0
 
     # Look at the start of the string - can it be "integerized"?
-    result = re.match(r"^(\d+)", reference)
+    result = re.match(r'^(\d+)', reference)
 
     if result and len(result.groups()) == 1:
         ref = result.groups()[0]
@@ -455,7 +455,7 @@ class InvenTreeAttachment(models.Model):
 
         Note: Re-implement this for each subclass of InvenTreeAttachment
         """
-        return "attachments"
+        return 'attachments'
 
     def save(self, *args, **kwargs):
         """Provide better validation error."""
@@ -547,7 +547,7 @@ class InvenTreeAttachment(models.Model):
             logger.error(
                 "Attempted to rename attachment outside valid directory: '%s'", new_file
             )
-            raise ValidationError(_("Invalid attachment directory"))
+            raise ValidationError(_('Invalid attachment directory'))
 
         # Ignore further checks if the filename is not actually being renamed
         if new_file == old_file:
@@ -556,23 +556,23 @@ class InvenTreeAttachment(models.Model):
         forbidden = [
             "'",
             '"',
-            "#",
-            "@",
-            "!",
-            "&",
-            "^",
-            "<",
-            ">",
-            ":",
-            ";",
-            "/",
-            "\\",
-            "|",
-            "?",
-            "*",
-            "%",
-            "~",
-            "`",
+            '#',
+            '@',
+            '!',
+            '&',
+            '^',
+            '<',
+            '>',
+            ':',
+            ';',
+            '/',
+            '\\',
+            '|',
+            '?',
+            '*',
+            '%',
+            '~',
+            '`',
         ]
 
         for c in forbidden:
@@ -580,7 +580,7 @@ class InvenTreeAttachment(models.Model):
                 raise ValidationError(_(f"Filename contains illegal character '{c}'"))
 
         if len(fn.split('.')) < 2:
-            raise ValidationError(_("Filename missing extension"))
+            raise ValidationError(_('Filename missing extension'))
 
         if not old_file.exists():
             logger.error(
@@ -589,14 +589,14 @@ class InvenTreeAttachment(models.Model):
             return
 
         if new_file.exists():
-            raise ValidationError(_("Attachment with this filename already exists"))
+            raise ValidationError(_('Attachment with this filename already exists'))
 
         try:
             os.rename(old_file, new_file)
             self.attachment.name = os.path.join(self.getSubdir(), fn)
             self.save()
         except Exception:
-            raise ValidationError(_("Error renaming file"))
+            raise ValidationError(_('Error renaming file'))
 
     def fully_qualified_url(self):
         """Return a 'fully qualified' URL for this attachment.
@@ -656,7 +656,7 @@ class InvenTreeTree(MPTTModel):
         except self.__class__.DoesNotExist:
             # If the object no longer exists, raise a ValidationError
             raise ValidationError(
-                "Object %s of type %s no longer exists", str(self), str(self.__class__)
+                'Object %s of type %s no longer exists', str(self), str(self.__class__)
             )
 
         # Cache node ID values for lower nodes, before we delete this one
@@ -791,7 +791,7 @@ class InvenTreeTree(MPTTModel):
             super().save(*args, **kwargs)
         except InvalidMove:
             # Provide better error for parent selection
-            raise ValidationError({'parent': _("Invalid choice")})
+            raise ValidationError({'parent': _('Invalid choice')})
 
         # Re-calculate the 'pathstring' field
         pathstring = self.construct_pathstring()
@@ -821,14 +821,14 @@ class InvenTreeTree(MPTTModel):
                 self.__class__.objects.bulk_update(nodes_to_update, ['pathstring'])
 
     name = models.CharField(
-        blank=False, max_length=100, verbose_name=_("Name"), help_text=_("Name")
+        blank=False, max_length=100, verbose_name=_('Name'), help_text=_('Name')
     )
 
     description = models.CharField(
         blank=True,
         max_length=250,
-        verbose_name=_("Description"),
-        help_text=_("Description (optional)"),
+        verbose_name=_('Description'),
+        help_text=_('Description (optional)'),
     )
 
     # When a category is deleted, graft the children onto its parent
@@ -837,7 +837,7 @@ class InvenTreeTree(MPTTModel):
         on_delete=models.DO_NOTHING,
         blank=True,
         null=True,
-        verbose_name=_("parent"),
+        verbose_name=_('parent'),
         related_name='children',
     )
 
@@ -854,7 +854,7 @@ class InvenTreeTree(MPTTModel):
 
         The default implementation returns an empty list
         """
-        raise NotImplementedError(f"items() method not implemented for {type(self)}")
+        raise NotImplementedError(f'items() method not implemented for {type(self)}')
 
     def getUniqueParents(self):
         """Return a flat set of all parent items that exist above this node.
@@ -929,7 +929,7 @@ class InvenTreeTree(MPTTModel):
 
     def __str__(self):
         """String representation of a category is the full path to that category."""
-        return f"{self.pathstring} - {self.description}"
+        return f'{self.pathstring} - {self.description}'
 
 
 class InvenTreeNotesMixin(models.Model):
@@ -1008,7 +1008,7 @@ class InvenTreeBarcodeMixin(models.Model):
 
         if hasattr(self, 'get_api_url'):
             api_url = self.get_api_url()
-            data['api_url'] = f"{api_url}{self.pk}/"
+            data['api_url'] = f'{api_url}{self.pk}/'
 
         if hasattr(self, 'get_absolute_url'):
             data['web_url'] = self.get_absolute_url()
@@ -1040,7 +1040,7 @@ class InvenTreeBarcodeMixin(models.Model):
         # Check for existing item
         if self.__class__.lookup_barcode(barcode_hash) is not None:
             if raise_error:
-                raise ValidationError(_("Existing barcode found"))
+                raise ValidationError(_('Existing barcode found'))
             else:
                 return False
 
diff --git a/InvenTree/InvenTree/permissions.py b/InvenTree/InvenTree/permissions.py
index 6aae0d3c23..74b844f008 100644
--- a/InvenTree/InvenTree/permissions.py
+++ b/InvenTree/InvenTree/permissions.py
@@ -18,7 +18,7 @@ def get_model_for_view(view, raise_error=True):
     if hasattr(view, 'get_serializer_class'):
         return view.get_serializr_class().Meta.model
 
-    raise AttributeError(f"Serializer class not specified for {view.__class__}")
+    raise AttributeError(f'Serializer class not specified for {view.__class__}')
 
 
 class RolePermission(permissions.BasePermission):
@@ -62,7 +62,7 @@ class RolePermission(permissions.BasePermission):
         }
 
         # let the view define a custom rolemap
-        if hasattr(view, "rolemap"):
+        if hasattr(view, 'rolemap'):
             rolemap.update(view.rolemap)
 
         permission = rolemap[request.method]
@@ -78,7 +78,7 @@ class RolePermission(permissions.BasePermission):
             app_label = model._meta.app_label
             model_name = model._meta.model_name
 
-            table = f"{app_label}_{model_name}"
+            table = f'{app_label}_{model_name}'
         except AttributeError:
             # We will assume that if the serializer class does *not* have a Meta,
             # then we don't need a permission
diff --git a/InvenTree/InvenTree/ready.py b/InvenTree/InvenTree/ready.py
index f6217c15e4..318d624fd9 100644
--- a/InvenTree/InvenTree/ready.py
+++ b/InvenTree/InvenTree/ready.py
@@ -25,8 +25,8 @@ def isInMainThread():
     - The RUN_MAIN env is set in that case. However if --noreload is applied, this variable
     is not set because there are no different threads.
     """
-    if "runserver" in sys.argv and "--noreload" not in sys.argv:
-        return os.environ.get('RUN_MAIN', None) == "true"
+    if 'runserver' in sys.argv and '--noreload' not in sys.argv:
+        return os.environ.get('RUN_MAIN', None) == 'true'
 
     return True
 
diff --git a/InvenTree/InvenTree/sentry.py b/InvenTree/InvenTree/sentry.py
index 8dc8c9305e..cd265f199d 100644
--- a/InvenTree/InvenTree/sentry.py
+++ b/InvenTree/InvenTree/sentry.py
@@ -37,7 +37,7 @@ def sentry_ignore_errors():
 
 def init_sentry(dsn, sample_rate, tags):
     """Initialize sentry.io error reporting"""
-    logger.info("Initializing sentry.io integration")
+    logger.info('Initializing sentry.io integration')
 
     sentry_sdk.init(
         dsn=dsn,
@@ -65,9 +65,9 @@ def report_exception(exc):
     """Report an exception to sentry.io"""
     if settings.SENTRY_ENABLED and settings.SENTRY_DSN:
         if not any(isinstance(exc, e) for e in sentry_ignore_errors()):
-            logger.info("Reporting exception to sentry.io: %s", exc)
+            logger.info('Reporting exception to sentry.io: %s', exc)
 
             try:
                 sentry_sdk.capture_exception(exc)
             except Exception:
-                logger.warning("Failed to report exception to sentry.io")
+                logger.warning('Failed to report exception to sentry.io')
diff --git a/InvenTree/InvenTree/serializers.py b/InvenTree/InvenTree/serializers.py
index c57b78e98d..824f11f25b 100644
--- a/InvenTree/InvenTree/serializers.py
+++ b/InvenTree/InvenTree/serializers.py
@@ -37,9 +37,9 @@ class InvenTreeMoneySerializer(MoneyField):
 
     def __init__(self, *args, **kwargs):
         """Override default values."""
-        kwargs["max_digits"] = kwargs.get("max_digits", 19)
-        self.decimal_places = kwargs["decimal_places"] = kwargs.get("decimal_places", 6)
-        kwargs["required"] = kwargs.get("required", False)
+        kwargs['max_digits'] = kwargs.get('max_digits', 19)
+        self.decimal_places = kwargs['decimal_places'] = kwargs.get('decimal_places', 6)
+        kwargs['required'] = kwargs.get('required', False)
 
         super().__init__(*args, **kwargs)
 
@@ -57,7 +57,7 @@ class InvenTreeMoneySerializer(MoneyField):
                 amount = Decimal(amount)
                 amount = round(amount, self.decimal_places)
         except Exception:
-            raise ValidationError({self.field_name: [_("Must be a valid number")]})
+            raise ValidationError({self.field_name: [_('Must be a valid number')]})
 
         currency = data.get(
             get_currency_field_name(self.field_name), self.default_currency
@@ -134,7 +134,7 @@ class DependentField(serializers.Field):
 
     def get_child(self, raise_exception=False):
         """This method tries to extract the child based on the provided data in the request by the client."""
-        data = deepcopy(self.context["request"].data)
+        data = deepcopy(self.context['request'].data)
 
         def visit_parent(node):
             """Recursively extract the data for the parent field/serializer in reverse."""
@@ -144,7 +144,7 @@ class DependentField(serializers.Field):
                 visit_parent(node.parent)
 
             # only do for composite fields and stop right before the current field
-            if hasattr(node, "child") and node is not self and isinstance(data, dict):
+            if hasattr(node, 'child') and node is not self and isinstance(data, dict):
                 data = data.get(node.field_name, None)
 
         visit_parent(self)
@@ -424,7 +424,7 @@ class ExendedUserSerializer(UserSerializer):
                 pass
             else:
                 raise PermissionDenied(
-                    _("You do not have permission to change this user role.")
+                    _('You do not have permission to change this user role.')
                 )
         return super().validate(attrs)
 
@@ -436,7 +436,7 @@ class UserCreateSerializer(ExendedUserSerializer):
         """Expanded valiadation for auth."""
         # Check that the user trying to create a new user is a superuser
         if not self.context['request'].user.is_superuser:
-            raise serializers.ValidationError(_("Only superusers can create new users"))
+            raise serializers.ValidationError(_('Only superusers can create new users'))
 
         # Generate a random password
         password = User.objects.make_random_password(length=14)
@@ -453,9 +453,9 @@ class UserCreateSerializer(ExendedUserSerializer):
         current_site = Site.objects.get_current()
         domain = current_site.domain
         instance.email_user(
-            subject=_(f"Welcome to {current_site.name}"),
+            subject=_(f'Welcome to {current_site.name}'),
             message=_(
-                f"Your account has been created.\n\nPlease use the password reset function to get access (at https://{domain})."
+                f'Your account has been created.\n\nPlease use the password reset function to get access (at https://{domain}).'
             ),
         )
         return instance
@@ -551,7 +551,7 @@ class InvenTreeDecimalField(serializers.FloatField):
         try:
             return Decimal(str(data))
         except Exception:
-            raise serializers.ValidationError(_("Invalid value"))
+            raise serializers.ValidationError(_('Invalid value'))
 
 
 class DataFileUploadSerializer(serializers.Serializer):
@@ -571,8 +571,8 @@ class DataFileUploadSerializer(serializers.Serializer):
         fields = ['data_file']
 
     data_file = serializers.FileField(
-        label=_("Data File"),
-        help_text=_("Select data file for upload"),
+        label=_('Data File'),
+        help_text=_('Select data file for upload'),
         required=True,
         allow_empty_file=False,
     )
@@ -589,13 +589,13 @@ class DataFileUploadSerializer(serializers.Serializer):
         accepted_file_types = ['xls', 'xlsx', 'csv', 'tsv', 'xml']
 
         if ext not in accepted_file_types:
-            raise serializers.ValidationError(_("Unsupported file type"))
+            raise serializers.ValidationError(_('Unsupported file type'))
 
         # Impose a 50MB limit on uploaded BOM files
         max_upload_file_size = 50 * 1024 * 1024
 
         if data_file.size > max_upload_file_size:
-            raise serializers.ValidationError(_("File is too large"))
+            raise serializers.ValidationError(_('File is too large'))
 
         # Read file data into memory (bytes object)
         try:
@@ -616,10 +616,10 @@ class DataFileUploadSerializer(serializers.Serializer):
             raise serializers.ValidationError(str(e))
 
         if len(self.dataset.headers) == 0:
-            raise serializers.ValidationError(_("No columns found in file"))
+            raise serializers.ValidationError(_('No columns found in file'))
 
         if len(self.dataset) == 0:
-            raise serializers.ValidationError(_("No data rows found in file"))
+            raise serializers.ValidationError(_('No data rows found in file'))
 
         return data_file
 
@@ -732,10 +732,10 @@ class DataFileExtractSerializer(serializers.Serializer):
         self.rows = data.get('rows', [])
 
         if len(self.rows) == 0:
-            raise serializers.ValidationError(_("No data rows provided"))
+            raise serializers.ValidationError(_('No data rows provided'))
 
         if len(self.columns) == 0:
-            raise serializers.ValidationError(_("No data columns supplied"))
+            raise serializers.ValidationError(_('No data columns supplied'))
 
         self.validate_extracted_columns()
 
@@ -758,7 +758,7 @@ class DataFileExtractSerializer(serializers.Serializer):
             processed_row = self.process_row(self.row_to_dict(row))
 
             if processed_row:
-                rows.append({"original": row, "data": processed_row})
+                rows.append({'original': row, 'data': processed_row})
 
         return {'fields': model_fields, 'columns': self.columns, 'rows': rows}
 
@@ -834,8 +834,8 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
         required=False,
         allow_blank=False,
         write_only=True,
-        label=_("Remote Image"),
-        help_text=_("URL of remote image file"),
+        label=_('Remote Image'),
+        help_text=_('URL of remote image file'),
     )
 
     def validate_remote_image(self, url):
@@ -851,7 +851,7 @@ class RemoteImageMixin(metaclass=serializers.SerializerMetaclass):
             'INVENTREE_DOWNLOAD_FROM_URL'
         ):
             raise ValidationError(
-                _("Downloading images from remote URL is not enabled")
+                _('Downloading images from remote URL is not enabled')
             )
 
         try:
diff --git a/InvenTree/InvenTree/settings.py b/InvenTree/InvenTree/settings.py
index cd0a294a2c..9e569bf069 100644
--- a/InvenTree/InvenTree/settings.py
+++ b/InvenTree/InvenTree/settings.py
@@ -52,7 +52,7 @@ if TESTING:
         site_packages = '/usr/local/lib/python3.9/site-packages'
 
         if site_packages not in sys.path:
-            print("Adding missing site-packages path:", site_packages)
+            print('Adding missing site-packages path:', site_packages)
             sys.path.append(site_packages)
 
 # Are environment variables manipulated by tests? Needs to be set by testing code
@@ -87,7 +87,7 @@ ENABLE_PLATFORM_FRONTEND = get_boolean_setting(
 # Configure logging settings
 log_level = get_setting('INVENTREE_LOG_LEVEL', 'log_level', 'WARNING')
 
-logging.basicConfig(level=log_level, format="%(asctime)s %(levelname)s %(message)s")
+logging.basicConfig(level=log_level, format='%(asctime)s %(levelname)s %(message)s')
 
 if log_level not in ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']:
     log_level = 'WARNING'  # pragma: no cover
@@ -109,7 +109,7 @@ if get_setting('INVENTREE_DB_LOGGING', 'db_logging', False):
     LOGGING['loggers'] = {'django.db.backends': {'level': log_level or 'DEBUG'}}
 
 # Get a logger instance for this setup file
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 # Load SECRET_KEY
 SECRET_KEY = config.get_secret_key()
@@ -122,7 +122,7 @@ MEDIA_ROOT = config.get_media_dir()
 
 # List of allowed hosts (default = allow all)
 ALLOWED_HOSTS = get_setting(
-    "INVENTREE_ALLOWED_HOSTS",
+    'INVENTREE_ALLOWED_HOSTS',
     config_key='allowed_hosts',
     default_value=['*'],
     typecast=list,
@@ -135,11 +135,11 @@ CORS_URLS_REGEX = r'^/(api|media|static)/.*$'
 
 # Extract CORS options from configuration file
 CORS_ORIGIN_ALLOW_ALL = get_boolean_setting(
-    "INVENTREE_CORS_ORIGIN_ALLOW_ALL", config_key='cors.allow_all', default_value=False
+    'INVENTREE_CORS_ORIGIN_ALLOW_ALL', config_key='cors.allow_all', default_value=False
 )
 
 CORS_ORIGIN_WHITELIST = get_setting(
-    "INVENTREE_CORS_ORIGIN_WHITELIST",
+    'INVENTREE_CORS_ORIGIN_WHITELIST',
     config_key='cors.whitelist',
     default_value=[],
     typecast=list,
@@ -279,43 +279,43 @@ AUTHENTICATION_BACKENDS = CONFIG.get(
         'django.contrib.auth.backends.RemoteUserBackend',  # proxy login
         'django.contrib.auth.backends.ModelBackend',
         'allauth.account.auth_backends.AuthenticationBackend',  # SSO login via external providers
-        "sesame.backends.ModelBackend",  # Magic link login django-sesame
+        'sesame.backends.ModelBackend',  # Magic link login django-sesame
     ],
 )
 
 # LDAP support
-LDAP_AUTH = get_boolean_setting("INVENTREE_LDAP_ENABLED", "ldap.enabled", False)
+LDAP_AUTH = get_boolean_setting('INVENTREE_LDAP_ENABLED', 'ldap.enabled', False)
 if LDAP_AUTH:
     import ldap
     from django_auth_ldap.config import GroupOfUniqueNamesType, LDAPSearch
 
-    AUTHENTICATION_BACKENDS.append("django_auth_ldap.backend.LDAPBackend")
+    AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend')
 
     # debug mode to troubleshoot configuration
-    LDAP_DEBUG = get_boolean_setting("INVENTREE_LDAP_DEBUG", "ldap.debug", False)
+    LDAP_DEBUG = get_boolean_setting('INVENTREE_LDAP_DEBUG', 'ldap.debug', False)
     if LDAP_DEBUG:
-        if "loggers" not in LOGGING:
-            LOGGING["loggers"] = {}
-        LOGGING["loggers"]["django_auth_ldap"] = {
-            "level": "DEBUG",
-            "handlers": ["console"],
+        if 'loggers' not in LOGGING:
+            LOGGING['loggers'] = {}
+        LOGGING['loggers']['django_auth_ldap'] = {
+            'level': 'DEBUG',
+            'handlers': ['console'],
         }
 
     # get global options from dict and use ldap.OPT_* as keys and values
     global_options_dict = get_setting(
-        "INVENTREE_LDAP_GLOBAL_OPTIONS", "ldap.global_options", {}, dict
+        'INVENTREE_LDAP_GLOBAL_OPTIONS', 'ldap.global_options', {}, dict
     )
     global_options = {}
     for k, v in global_options_dict.items():
         # keys are always ldap.OPT_* constants
         k_attr = getattr(ldap, k, None)
-        if not k.startswith("OPT_") or k_attr is None:
+        if not k.startswith('OPT_') or k_attr is None:
             print(f"[LDAP] ldap.global_options, key '{k}' not found, skipping...")
             continue
 
         # values can also be other strings, e.g. paths
         v_attr = v
-        if v.startswith("OPT_"):
+        if v.startswith('OPT_'):
             v_attr = getattr(ldap, v, None)
 
         if v_attr is None:
@@ -325,55 +325,55 @@ if LDAP_AUTH:
         global_options[k_attr] = v_attr
     AUTH_LDAP_GLOBAL_OPTIONS = global_options
     if LDAP_DEBUG:
-        print("[LDAP] ldap.global_options =", global_options)
+        print('[LDAP] ldap.global_options =', global_options)
 
-    AUTH_LDAP_SERVER_URI = get_setting("INVENTREE_LDAP_SERVER_URI", "ldap.server_uri")
+    AUTH_LDAP_SERVER_URI = get_setting('INVENTREE_LDAP_SERVER_URI', 'ldap.server_uri')
     AUTH_LDAP_START_TLS = get_boolean_setting(
-        "INVENTREE_LDAP_START_TLS", "ldap.start_tls", False
+        'INVENTREE_LDAP_START_TLS', 'ldap.start_tls', False
     )
-    AUTH_LDAP_BIND_DN = get_setting("INVENTREE_LDAP_BIND_DN", "ldap.bind_dn")
+    AUTH_LDAP_BIND_DN = get_setting('INVENTREE_LDAP_BIND_DN', 'ldap.bind_dn')
     AUTH_LDAP_BIND_PASSWORD = get_setting(
-        "INVENTREE_LDAP_BIND_PASSWORD", "ldap.bind_password"
+        'INVENTREE_LDAP_BIND_PASSWORD', 'ldap.bind_password'
     )
     AUTH_LDAP_USER_SEARCH = LDAPSearch(
-        get_setting("INVENTREE_LDAP_SEARCH_BASE_DN", "ldap.search_base_dn"),
+        get_setting('INVENTREE_LDAP_SEARCH_BASE_DN', 'ldap.search_base_dn'),
         ldap.SCOPE_SUBTREE,
         str(
             get_setting(
-                "INVENTREE_LDAP_SEARCH_FILTER_STR",
-                "ldap.search_filter_str",
-                "(uid= %(user)s)",
+                'INVENTREE_LDAP_SEARCH_FILTER_STR',
+                'ldap.search_filter_str',
+                '(uid= %(user)s)',
             )
         ),
     )
     AUTH_LDAP_USER_DN_TEMPLATE = get_setting(
-        "INVENTREE_LDAP_USER_DN_TEMPLATE", "ldap.user_dn_template"
+        'INVENTREE_LDAP_USER_DN_TEMPLATE', 'ldap.user_dn_template'
     )
     AUTH_LDAP_USER_ATTR_MAP = get_setting(
-        "INVENTREE_LDAP_USER_ATTR_MAP",
-        "ldap.user_attr_map",
+        'INVENTREE_LDAP_USER_ATTR_MAP',
+        'ldap.user_attr_map',
         {'first_name': 'givenName', 'last_name': 'sn', 'email': 'mail'},
         dict,
     )
     AUTH_LDAP_ALWAYS_UPDATE_USER = get_boolean_setting(
-        "INVENTREE_LDAP_ALWAYS_UPDATE_USER", "ldap.always_update_user", True
+        'INVENTREE_LDAP_ALWAYS_UPDATE_USER', 'ldap.always_update_user', True
     )
     AUTH_LDAP_CACHE_TIMEOUT = get_setting(
-        "INVENTREE_LDAP_CACHE_TIMEOUT", "ldap.cache_timeout", 3600, int
+        'INVENTREE_LDAP_CACHE_TIMEOUT', 'ldap.cache_timeout', 3600, int
     )
 
     AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
-        get_setting("INVENTREE_LDAP_GROUP_SEARCH", "ldap.group_search"),
+        get_setting('INVENTREE_LDAP_GROUP_SEARCH', 'ldap.group_search'),
         ldap.SCOPE_SUBTREE,
-        "(objectClass=groupOfUniqueNames)",
+        '(objectClass=groupOfUniqueNames)',
     )
-    AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType(name_attr="cn")
+    AUTH_LDAP_GROUP_TYPE = GroupOfUniqueNamesType(name_attr='cn')
     AUTH_LDAP_REQUIRE_GROUP = get_setting(
-        "INVENTREE_LDAP_REQUIRE_GROUP", "ldap.require_group"
+        'INVENTREE_LDAP_REQUIRE_GROUP', 'ldap.require_group'
     )
-    AUTH_LDAP_DENY_GROUP = get_setting("INVENTREE_LDAP_DENY_GROUP", "ldap.deny_group")
+    AUTH_LDAP_DENY_GROUP = get_setting('INVENTREE_LDAP_DENY_GROUP', 'ldap.deny_group')
     AUTH_LDAP_USER_FLAGS_BY_GROUP = get_setting(
-        "INVENTREE_LDAP_USER_FLAGS_BY_GROUP", "ldap.user_flags_by_group", {}, dict
+        'INVENTREE_LDAP_USER_FLAGS_BY_GROUP', 'ldap.user_flags_by_group', {}, dict
     )
     AUTH_LDAP_FIND_GROUP_PERMS = True
 
@@ -383,7 +383,7 @@ DEBUG_TOOLBAR_ENABLED = DEBUG and get_setting(
 
 # If the debug toolbar is enabled, add the modules
 if DEBUG_TOOLBAR_ENABLED:  # pragma: no cover
-    logger.info("Running with DEBUG_TOOLBAR enabled")
+    logger.info('Running with DEBUG_TOOLBAR enabled')
     INSTALLED_APPS.append('debug_toolbar')
     MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
 
@@ -401,9 +401,9 @@ DOCKER = get_boolean_setting('INVENTREE_DOCKER', default_value=False)
 if DOCKER:  # pragma: no cover
     # Internal IP addresses are different when running under docker
     hostname, ___, ips = socket.gethostbyname_ex(socket.gethostname())
-    INTERNAL_IPS = [ip[: ip.rfind(".")] + ".1" for ip in ips] + [
-        "127.0.0.1",
-        "10.0.2.2",
+    INTERNAL_IPS = [ip[: ip.rfind('.')] + '.1' for ip in ips] + [
+        '127.0.0.1',
+        '10.0.2.2',
     ]
 
 # Allow secure http developer server in debug mode
@@ -521,7 +521,7 @@ Configure the database backend based on the user-specified values.
 - The following code lets the user "mix and match" database configuration
 """
 
-logger.debug("Configuring database backend:")
+logger.debug('Configuring database backend:')
 
 # Extract database configuration from the config.yaml file
 db_config = CONFIG.get('database', {})
@@ -535,7 +535,7 @@ db_keys = ['ENGINE', 'NAME', 'USER', 'PASSWORD', 'HOST', 'PORT']
 
 for key in db_keys:
     # First, check the environment variables
-    env_key = f"INVENTREE_DB_{key}"
+    env_key = f'INVENTREE_DB_{key}'
     env_var = os.environ.get(env_key, None)
 
     if env_var:
@@ -544,7 +544,7 @@ for key in db_keys:
             try:
                 env_var = int(env_var)
             except ValueError:
-                logger.exception("Invalid number for %s: %s", env_key, env_var)
+                logger.exception('Invalid number for %s: %s', env_key, env_var)
         # Override configuration value
         db_config[key] = env_var
 
@@ -585,9 +585,9 @@ if 'sqlite' in db_engine:
     db_name = str(Path(db_name).resolve())
     db_config['NAME'] = db_name
 
-logger.info("DB_ENGINE: %s", db_engine)
-logger.info("DB_NAME: %s", db_name)
-logger.info("DB_HOST: %s", db_host)
+logger.info('DB_ENGINE: %s', db_engine)
+logger.info('DB_NAME: %s', db_name)
+logger.info('DB_HOST: %s', db_host)
 
 """
 In addition to base-level database configuration, we may wish to specify specific options to the database backend
@@ -600,21 +600,21 @@ Ref: https://docs.djangoproject.com/en/3.2/ref/settings/#std:setting-OPTIONS
 # connecting to the database server (such as a replica failover) don't sit and
 # wait for possibly an hour or more, just tell the client something went wrong
 # and let the client retry when they want to.
-db_options = db_config.get("OPTIONS", db_config.get("options", {}))
+db_options = db_config.get('OPTIONS', db_config.get('options', {}))
 
 # Specific options for postgres backend
-if "postgres" in db_engine:  # pragma: no cover
+if 'postgres' in db_engine:  # pragma: no cover
     from psycopg2.extensions import (
         ISOLATION_LEVEL_READ_COMMITTED,
         ISOLATION_LEVEL_SERIALIZABLE,
     )
 
     # Connection timeout
-    if "connect_timeout" not in db_options:
+    if 'connect_timeout' not in db_options:
         # The DB server is in the same data center, it should not take very
         # long to connect to the database server
         # # seconds, 2 is minimum allowed by libpq
-        db_options["connect_timeout"] = int(
+        db_options['connect_timeout'] = int(
             get_setting('INVENTREE_DB_TIMEOUT', 'database.timeout', 2)
         )
 
@@ -624,36 +624,36 @@ if "postgres" in db_engine:  # pragma: no cover
     # issue to resolve itself.  It it that doesn't happen whatever happened
     # is probably fatal and no amount of waiting is going to fix it.
     # # 0 - TCP Keepalives disabled; 1 - enabled
-    if "keepalives" not in db_options:
-        db_options["keepalives"] = int(
+    if 'keepalives' not in db_options:
+        db_options['keepalives'] = int(
             get_setting('INVENTREE_DB_TCP_KEEPALIVES', 'database.tcp_keepalives', 1)
         )
 
     # Seconds after connection is idle to send keep alive
-    if "keepalives_idle" not in db_options:
-        db_options["keepalives_idle"] = int(
+    if 'keepalives_idle' not in db_options:
+        db_options['keepalives_idle'] = int(
             get_setting(
                 'INVENTREE_DB_TCP_KEEPALIVES_IDLE', 'database.tcp_keepalives_idle', 1
             )
         )
 
     # Seconds after missing ACK to send another keep alive
-    if "keepalives_interval" not in db_options:
-        db_options["keepalives_interval"] = int(
+    if 'keepalives_interval' not in db_options:
+        db_options['keepalives_interval'] = int(
             get_setting(
-                "INVENTREE_DB_TCP_KEEPALIVES_INTERVAL",
-                "database.tcp_keepalives_internal",
-                "1",
+                'INVENTREE_DB_TCP_KEEPALIVES_INTERVAL',
+                'database.tcp_keepalives_internal',
+                '1',
             )
         )
 
     # Number of missing ACKs before we close the connection
-    if "keepalives_count" not in db_options:
-        db_options["keepalives_count"] = int(
+    if 'keepalives_count' not in db_options:
+        db_options['keepalives_count'] = int(
             get_setting(
-                "INVENTREE_DB_TCP_KEEPALIVES_COUNT",
-                "database.tcp_keepalives_count",
-                "5",
+                'INVENTREE_DB_TCP_KEEPALIVES_COUNT',
+                'database.tcp_keepalives_count',
+                '5',
             )
         )
 
@@ -668,18 +668,18 @@ if "postgres" in db_engine:  # pragma: no cover
     # protect against simultaneous changes.
     # https://www.postgresql.org/docs/devel/transaction-iso.html
     # https://docs.djangoproject.com/en/3.2/ref/databases/#isolation-level
-    if "isolation_level" not in db_options:
+    if 'isolation_level' not in db_options:
         serializable = get_boolean_setting(
             'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False
         )
-        db_options["isolation_level"] = (
+        db_options['isolation_level'] = (
             ISOLATION_LEVEL_SERIALIZABLE
             if serializable
             else ISOLATION_LEVEL_READ_COMMITTED
         )
 
 # Specific options for MySql / MariaDB backend
-elif "mysql" in db_engine:  # pragma: no cover
+elif 'mysql' in db_engine:  # pragma: no cover
     # TODO TCP time outs and keepalives
 
     # MariaDB's default isolation level is Repeatable Read which is
@@ -688,16 +688,16 @@ elif "mysql" in db_engine:  # pragma: no cover
     # protect against siumltaneous changes.
     # https://mariadb.com/kb/en/mariadb-transactions-and-isolation-levels-for-sql-server-users/#changing-the-isolation-level
     # https://docs.djangoproject.com/en/3.2/ref/databases/#mysql-isolation-level
-    if "isolation_level" not in db_options:
+    if 'isolation_level' not in db_options:
         serializable = get_boolean_setting(
             'INVENTREE_DB_ISOLATION_SERIALIZABLE', 'database.serializable', False
         )
-        db_options["isolation_level"] = (
-            "serializable" if serializable else "read committed"
+        db_options['isolation_level'] = (
+            'serializable' if serializable else 'read committed'
         )
 
 # Specific options for sqlite backend
-elif "sqlite" in db_engine:
+elif 'sqlite' in db_engine:
     # TODO: Verify timeouts are not an issue because no network is involved for SQLite
 
     # SQLite's default isolation level is Serializable due to SQLite's
@@ -756,30 +756,30 @@ if cache_host:  # pragma: no cover
     # so don't wait too long for the cache as nothing in the cache should be
     # irreplaceable.
     _cache_options = {
-        "CLIENT_CLASS": "django_redis.client.DefaultClient",
-        "SOCKET_CONNECT_TIMEOUT": int(os.getenv("CACHE_CONNECT_TIMEOUT", "2")),
-        "SOCKET_TIMEOUT": int(os.getenv("CACHE_SOCKET_TIMEOUT", "2")),
-        "CONNECTION_POOL_KWARGS": {
-            "socket_keepalive": config.is_true(os.getenv("CACHE_TCP_KEEPALIVE", "1")),
-            "socket_keepalive_options": {
-                socket.TCP_KEEPCNT: int(os.getenv("CACHE_KEEPALIVES_COUNT", "5")),
-                socket.TCP_KEEPIDLE: int(os.getenv("CACHE_KEEPALIVES_IDLE", "1")),
-                socket.TCP_KEEPINTVL: int(os.getenv("CACHE_KEEPALIVES_INTERVAL", "1")),
+        'CLIENT_CLASS': 'django_redis.client.DefaultClient',
+        'SOCKET_CONNECT_TIMEOUT': int(os.getenv('CACHE_CONNECT_TIMEOUT', '2')),
+        'SOCKET_TIMEOUT': int(os.getenv('CACHE_SOCKET_TIMEOUT', '2')),
+        'CONNECTION_POOL_KWARGS': {
+            'socket_keepalive': config.is_true(os.getenv('CACHE_TCP_KEEPALIVE', '1')),
+            'socket_keepalive_options': {
+                socket.TCP_KEEPCNT: int(os.getenv('CACHE_KEEPALIVES_COUNT', '5')),
+                socket.TCP_KEEPIDLE: int(os.getenv('CACHE_KEEPALIVES_IDLE', '1')),
+                socket.TCP_KEEPINTVL: int(os.getenv('CACHE_KEEPALIVES_INTERVAL', '1')),
                 socket.TCP_USER_TIMEOUT: int(
-                    os.getenv("CACHE_TCP_USER_TIMEOUT", "1000")
+                    os.getenv('CACHE_TCP_USER_TIMEOUT', '1000')
                 ),
             },
         },
     }
     CACHES = {
-        "default": {
-            "BACKEND": "django_redis.cache.RedisCache",
-            "LOCATION": f"redis://{cache_host}:{cache_port}/0",
-            "OPTIONS": _cache_options,
+        'default': {
+            'BACKEND': 'django_redis.cache.RedisCache',
+            'LOCATION': f'redis://{cache_host}:{cache_port}/0',
+            'OPTIONS': _cache_options,
         }
     }
 else:
-    CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}}
+    CACHES = {'default': {'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'}}
 
 _q_worker_timeout = int(
     get_setting('INVENTREE_BACKGROUND_TIMEOUT', 'background.timeout', 90)
@@ -813,7 +813,7 @@ if SENTRY_ENABLED and SENTRY_DSN:
 if cache_host:  # pragma: no cover
     # If using external redis cache, make the cache the broker for Django Q
     # as well
-    Q_CLUSTER["django_redis"] = "worker"
+    Q_CLUSTER['django_redis'] = 'worker'
 
 # database user sessions
 SESSION_ENGINE = 'user_sessions.backends.db'
@@ -840,7 +840,7 @@ AUTH_PASSWORD_VALIDATORS = [
 EXTRA_URL_SCHEMES = get_setting('INVENTREE_EXTRA_URL_SCHEMES', 'extra_url_schemes', [])
 
 if type(EXTRA_URL_SCHEMES) not in [list]:  # pragma: no cover
-    logger.warning("extra_url_schemes not correctly formatted")
+    logger.warning('extra_url_schemes not correctly formatted')
     EXTRA_URL_SCHEMES = []
 
 # Internationalization
@@ -912,7 +912,7 @@ CURRENCIES = get_setting(
 
 # Ensure that at least one currency value is available
 if len(CURRENCIES) == 0:  # pragma: no cover
-    logger.warning("No currencies selected: Defaulting to USD")
+    logger.warning('No currencies selected: Defaulting to USD')
     CURRENCIES = ['USD']
 
 # Maximum number of decimal places for currency rendering
@@ -965,7 +965,7 @@ USE_L10N = True
 if not TESTING:
     USE_TZ = True  # pragma: no cover
 
-DATE_INPUT_FORMATS = ["%Y-%m-%d"]
+DATE_INPUT_FORMATS = ['%Y-%m-%d']
 
 # crispy forms use the bootstrap templates
 CRISPY_TEMPLATE_PACK = 'bootstrap4'
@@ -1090,7 +1090,7 @@ PLUGIN_FILE_CHECKED = False  # Was the plugin file checked?
 SITE_URL = get_setting('INVENTREE_SITE_URL', 'site_url', None)
 
 if SITE_URL:
-    logger.info("Site URL: %s", SITE_URL)
+    logger.info('Site URL: %s', SITE_URL)
 
     # Check that the site URL is valid
     validator = URLValidator()
@@ -1111,7 +1111,7 @@ FRONTEND_SETTINGS = config.get_frontend_settings(debug=DEBUG)
 FRONTEND_URL_BASE = FRONTEND_SETTINGS.get('base_url', 'platform')
 
 if DEBUG:
-    logger.info("InvenTree running with DEBUG enabled")
+    logger.info('InvenTree running with DEBUG enabled')
 
 logger.info("MEDIA_ROOT: '%s'", MEDIA_ROOT)
 logger.info("STATIC_ROOT: '%s'", STATIC_ROOT)
@@ -1131,12 +1131,12 @@ FLAGS = {
 CUSTOM_FLAGS = get_setting('INVENTREE_FLAGS', 'flags', None, typecast=dict)
 if CUSTOM_FLAGS:
     if not isinstance(CUSTOM_FLAGS, dict):
-        logger.error("Invalid custom flags, must be valid dict: %s", str(CUSTOM_FLAGS))
+        logger.error('Invalid custom flags, must be valid dict: %s', str(CUSTOM_FLAGS))
     else:
-        logger.info("Custom flags: %s", str(CUSTOM_FLAGS))
+        logger.info('Custom flags: %s', str(CUSTOM_FLAGS))
         FLAGS.update(CUSTOM_FLAGS)
 
 # Magic login django-sesame
 SESAME_MAX_AGE = 300
 # LOGIN_REDIRECT_URL = f"/{FRONTEND_URL_BASE}/logged-in/"
-LOGIN_REDIRECT_URL = "/index/"
+LOGIN_REDIRECT_URL = '/index/'
diff --git a/InvenTree/InvenTree/social_auth_urls.py b/InvenTree/InvenTree/social_auth_urls.py
index a5ca10c777..e323b1892d 100644
--- a/InvenTree/InvenTree/social_auth_urls.py
+++ b/InvenTree/InvenTree/social_auth_urls.py
@@ -74,9 +74,9 @@ provider_urlpatterns = []
 
 for name, provider in providers.registry.provider_map.items():
     try:
-        prov_mod = import_module(provider.get_package() + ".views")
+        prov_mod = import_module(provider.get_package() + '.views')
     except ImportError:
-        logger.exception("Could not import authentication provider %s", name)
+        logger.exception('Could not import authentication provider %s', name)
         continue
 
     # Try to extract the adapter class
diff --git a/InvenTree/InvenTree/sso.py b/InvenTree/InvenTree/sso.py
index 57fffa367d..d77a7dfc1b 100644
--- a/InvenTree/InvenTree/sso.py
+++ b/InvenTree/InvenTree/sso.py
@@ -48,7 +48,7 @@ def check_provider(provider, raise_error=False):
     if allauth.app_settings.SITES_ENABLED:
         # At least one matching site must be specified
         if not app.sites.exists():
-            logger.error("SocialApp %s has no sites configured", app)
+            logger.error('SocialApp %s has no sites configured', app)
             return False
 
     # At this point, we assume that the provider is correctly configured
diff --git a/InvenTree/InvenTree/status.py b/InvenTree/InvenTree/status.py
index bd23ac1fa6..cd6d357928 100644
--- a/InvenTree/InvenTree/status.py
+++ b/InvenTree/InvenTree/status.py
@@ -13,7 +13,7 @@ from django_q.status import Stat
 import InvenTree.email
 import InvenTree.ready
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 def is_worker_running(**kwargs):
@@ -63,13 +63,13 @@ def check_system_health(**kwargs):
 
     if not is_worker_running(**kwargs):  # pragma: no cover
         result = False
-        logger.warning(_("Background worker check failed"))
+        logger.warning(_('Background worker check failed'))
 
     if not InvenTree.email.is_email_configured():  # pragma: no cover
         result = False
-        logger.warning(_("Email backend not configured"))
+        logger.warning(_('Email backend not configured'))
 
     if not result:  # pragma: no cover
-        logger.warning(_("InvenTree system health checks failed"))
+        logger.warning(_('InvenTree system health checks failed'))
 
     return result
diff --git a/InvenTree/InvenTree/status_codes.py b/InvenTree/InvenTree/status_codes.py
index 966833f78f..65a02fb4b2 100644
--- a/InvenTree/InvenTree/status_codes.py
+++ b/InvenTree/InvenTree/status_codes.py
@@ -9,12 +9,12 @@ class PurchaseOrderStatus(StatusCode):
     """Defines a set of status codes for a PurchaseOrder."""
 
     # Order status codes
-    PENDING = 10, _("Pending"), 'secondary'  # Order is pending (not yet placed)
-    PLACED = 20, _("Placed"), 'primary'  # Order has been placed with supplier
-    COMPLETE = 30, _("Complete"), 'success'  # Order has been completed
-    CANCELLED = 40, _("Cancelled"), 'danger'  # Order was cancelled
-    LOST = 50, _("Lost"), 'warning'  # Order was lost
-    RETURNED = 60, _("Returned"), 'warning'  # Order was returned
+    PENDING = 10, _('Pending'), 'secondary'  # Order is pending (not yet placed)
+    PLACED = 20, _('Placed'), 'primary'  # Order has been placed with supplier
+    COMPLETE = 30, _('Complete'), 'success'  # Order has been completed
+    CANCELLED = 40, _('Cancelled'), 'danger'  # Order was cancelled
+    LOST = 50, _('Lost'), 'warning'  # Order was lost
+    RETURNED = 60, _('Returned'), 'warning'  # Order was returned
 
 
 class PurchaseOrderStatusGroups:
@@ -34,16 +34,16 @@ class PurchaseOrderStatusGroups:
 class SalesOrderStatus(StatusCode):
     """Defines a set of status codes for a SalesOrder."""
 
-    PENDING = 10, _("Pending"), 'secondary'  # Order is pending
+    PENDING = 10, _('Pending'), 'secondary'  # Order is pending
     IN_PROGRESS = (
         15,
-        _("In Progress"),
+        _('In Progress'),
         'primary',
     )  # Order has been issued, and is in progress
-    SHIPPED = 20, _("Shipped"), 'success'  # Order has been shipped to customer
-    CANCELLED = 40, _("Cancelled"), 'danger'  # Order has been cancelled
-    LOST = 50, _("Lost"), 'warning'  # Order was lost
-    RETURNED = 60, _("Returned"), 'warning'  # Order was returned
+    SHIPPED = 20, _('Shipped'), 'success'  # Order has been shipped to customer
+    CANCELLED = 40, _('Cancelled'), 'danger'  # Order has been cancelled
+    LOST = 50, _('Lost'), 'warning'  # Order was lost
+    RETURNED = 60, _('Returned'), 'warning'  # Order was returned
 
 
 class SalesOrderStatusGroups:
@@ -59,18 +59,18 @@ class SalesOrderStatusGroups:
 class StockStatus(StatusCode):
     """Status codes for Stock."""
 
-    OK = 10, _("OK"), 'success'  # Item is OK
-    ATTENTION = 50, _("Attention needed"), 'warning'  # Item requires attention
-    DAMAGED = 55, _("Damaged"), 'warning'  # Item is damaged
-    DESTROYED = 60, _("Destroyed"), 'danger'  # Item is destroyed
-    REJECTED = 65, _("Rejected"), 'danger'  # Item is rejected
-    LOST = 70, _("Lost"), 'dark'  # Item has been lost
+    OK = 10, _('OK'), 'success'  # Item is OK
+    ATTENTION = 50, _('Attention needed'), 'warning'  # Item requires attention
+    DAMAGED = 55, _('Damaged'), 'warning'  # Item is damaged
+    DESTROYED = 60, _('Destroyed'), 'danger'  # Item is destroyed
+    REJECTED = 65, _('Rejected'), 'danger'  # Item is rejected
+    LOST = 70, _('Lost'), 'dark'  # Item has been lost
     QUARANTINED = (
         75,
-        _("Quarantined"),
+        _('Quarantined'),
         'info',
     )  # Item has been quarantined and is unavailable
-    RETURNED = 85, _("Returned"), 'warning'  # Item has been returned from a customer
+    RETURNED = 85, _('Returned'), 'warning'  # Item has been returned from a customer
 
 
 class StockStatusGroups:
@@ -129,7 +129,7 @@ class StockHistoryCode(StatusCode):
     BUILD_CONSUMED = 57, _('Consumed by build order')
 
     # Sales order codes
-    SHIPPED_AGAINST_SALES_ORDER = 60, _("Shipped against Sales Order")
+    SHIPPED_AGAINST_SALES_ORDER = 60, _('Shipped against Sales Order')
 
     # Purchase order codes
     RECEIVED_AGAINST_PURCHASE_ORDER = 70, _('Received against Purchase Order')
@@ -145,10 +145,10 @@ class StockHistoryCode(StatusCode):
 class BuildStatus(StatusCode):
     """Build status codes."""
 
-    PENDING = 10, _("Pending"), 'secondary'  # Build is pending / active
-    PRODUCTION = 20, _("Production"), 'primary'  # BuildOrder is in production
-    CANCELLED = 30, _("Cancelled"), 'danger'  # Build was cancelled
-    COMPLETE = 40, _("Complete"), 'success'  # Build is complete
+    PENDING = 10, _('Pending'), 'secondary'  # Build is pending / active
+    PRODUCTION = 20, _('Production'), 'primary'  # BuildOrder is in production
+    CANCELLED = 30, _('Cancelled'), 'danger'  # Build was cancelled
+    COMPLETE = 40, _('Complete'), 'success'  # Build is complete
 
 
 class BuildStatusGroups:
@@ -161,13 +161,13 @@ class ReturnOrderStatus(StatusCode):
     """Defines a set of status codes for a ReturnOrder"""
 
     # Order is pending, waiting for receipt of items
-    PENDING = 10, _("Pending"), 'secondary'
+    PENDING = 10, _('Pending'), 'secondary'
 
     # Items have been received, and are being inspected
-    IN_PROGRESS = 20, _("In Progress"), 'primary'
+    IN_PROGRESS = 20, _('In Progress'), 'primary'
 
-    COMPLETE = 30, _("Complete"), 'success'
-    CANCELLED = 40, _("Cancelled"), 'danger'
+    COMPLETE = 30, _('Complete'), 'success'
+    CANCELLED = 40, _('Cancelled'), 'danger'
 
 
 class ReturnOrderStatusGroups:
@@ -179,19 +179,19 @@ class ReturnOrderStatusGroups:
 class ReturnOrderLineStatus(StatusCode):
     """Defines a set of status codes for a ReturnOrderLineItem"""
 
-    PENDING = 10, _("Pending"), 'secondary'
+    PENDING = 10, _('Pending'), 'secondary'
 
     # Item is to be returned to customer, no other action
-    RETURN = 20, _("Return"), 'success'
+    RETURN = 20, _('Return'), 'success'
 
     # Item is to be repaired, and returned to customer
-    REPAIR = 30, _("Repair"), 'primary'
+    REPAIR = 30, _('Repair'), 'primary'
 
     # Item is to be replaced (new item shipped)
-    REPLACE = 40, _("Replace"), 'warning'
+    REPLACE = 40, _('Replace'), 'warning'
 
     # Item is to be refunded (cannot be repaired)
-    REFUND = 50, _("Refund"), 'info'
+    REFUND = 50, _('Refund'), 'info'
 
     # Item is rejected
-    REJECT = 60, _("Reject"), 'danger'
+    REJECT = 60, _('Reject'), 'danger'
diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py
index 975065a295..7f67ab9f63 100644
--- a/InvenTree/InvenTree/tasks.py
+++ b/InvenTree/InvenTree/tasks.py
@@ -31,7 +31,7 @@ from plugin import registry
 
 from .version import isInvenTreeUpToDate
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 def schedule_task(taskname, **kwargs):
@@ -46,7 +46,7 @@ def schedule_task(taskname, **kwargs):
     try:
         from django_q.models import Schedule
     except AppRegistryNotReady:  # pragma: no cover
-        logger.info("Could not start background tasks - App registry not ready")
+        logger.info('Could not start background tasks - App registry not ready')
         return
 
     try:
@@ -278,13 +278,13 @@ class ScheduledTask:
     interval: str
     minutes: int = None
 
-    MINUTES = "I"
-    HOURLY = "H"
-    DAILY = "D"
-    WEEKLY = "W"
-    MONTHLY = "M"
-    QUARTERLY = "Q"
-    YEARLY = "Y"
+    MINUTES = 'I'
+    HOURLY = 'H'
+    DAILY = 'D'
+    WEEKLY = 'W'
+    MONTHLY = 'M'
+    QUARTERLY = 'Q'
+    YEARLY = 'Y'
     TYPE = [MINUTES, HOURLY, DAILY, WEEKLY, MONTHLY, QUARTERLY, YEARLY]
 
 
@@ -349,7 +349,7 @@ def heartbeat():
     try:
         from django_q.models import Success
     except AppRegistryNotReady:  # pragma: no cover
-        logger.info("Could not perform heartbeat task - App registry not ready")
+        logger.info('Could not perform heartbeat task - App registry not ready')
         return
 
     threshold = timezone.now() - timedelta(minutes=30)
@@ -378,7 +378,7 @@ def delete_successful_tasks():
         results = Success.objects.filter(started__lte=threshold)
 
         if results.count() > 0:
-            logger.info("Deleting %s successful task records", results.count())
+            logger.info('Deleting %s successful task records', results.count())
             results.delete()
 
     except AppRegistryNotReady:  # pragma: no cover
@@ -402,7 +402,7 @@ def delete_failed_tasks():
         results = Failure.objects.filter(started__lte=threshold)
 
         if results.count() > 0:
-            logger.info("Deleting %s failed task records", results.count())
+            logger.info('Deleting %s failed task records', results.count())
             results.delete()
 
     except AppRegistryNotReady:  # pragma: no cover
@@ -423,7 +423,7 @@ def delete_old_error_logs():
         errors = Error.objects.filter(when__lte=threshold)
 
         if errors.count() > 0:
-            logger.info("Deleting %s old error logs", errors.count())
+            logger.info('Deleting %s old error logs', errors.count())
             errors.delete()
 
     except AppRegistryNotReady:  # pragma: no cover
@@ -449,13 +449,13 @@ def delete_old_notifications():
         items = NotificationEntry.objects.filter(updated__lte=threshold)
 
         if items.count() > 0:
-            logger.info("Deleted %s old notification entries", items.count())
+            logger.info('Deleted %s old notification entries', items.count())
             items.delete()
 
         items = NotificationMessage.objects.filter(creation__lte=threshold)
 
         if items.count() > 0:
-            logger.info("Deleted %s old notification messages", items.count())
+            logger.info('Deleted %s old notification messages', items.count())
             items.delete()
 
     except AppRegistryNotReady:
@@ -485,7 +485,7 @@ def check_for_updates():
     if not check_daily_holdoff('check_for_updates', interval):
         return
 
-    logger.info("Checking for InvenTree software updates")
+    logger.info('Checking for InvenTree software updates')
 
     headers = {}
 
@@ -494,7 +494,7 @@ def check_for_updates():
         token = os.getenv('GITHUB_TOKEN', None)
 
         if token:
-            headers['Authorization'] = f"Bearer {token}"
+            headers['Authorization'] = f'Bearer {token}'
 
     response = requests.get(
         'https://api.github.com/repos/inventree/inventree/releases/latest',
@@ -513,7 +513,7 @@ def check_for_updates():
     if not tag:
         raise ValueError("'tag_name' missing from GitHub response")  # pragma: no cover
 
-    match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
+    match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', tag)
 
     if len(match.groups()) != 3:  # pragma: no cover
         logger.warning("Version '%s' did not match expected pattern", tag)
@@ -534,15 +534,15 @@ def check_for_updates():
 
     # Send notification if there is a new version
     if not isInvenTreeUpToDate():
-        logger.warning("InvenTree is not up-to-date, sending notification")
+        logger.warning('InvenTree is not up-to-date, sending notification')
 
         plg = registry.get_plugin('InvenTreeCoreNotificationsPlugin')
         if not plg:
-            logger.warning("Cannot send notification - plugin not found")
+            logger.warning('Cannot send notification - plugin not found')
             return
         plg = plg.plugin_config()
         if not plg:
-            logger.warning("Cannot send notification - plugin config not found")
+            logger.warning('Cannot send notification - plugin config not found')
             return
         # Send notification
         trigger_superuser_notification(
@@ -579,7 +579,7 @@ def update_exchange_rates(force: bool = False):
         )
 
         if not check_daily_holdoff('update_exchange_rates', interval):
-            logger.info("Skipping exchange rate update (interval not reached)")
+            logger.info('Skipping exchange rate update (interval not reached)')
             return
 
     backend = InvenTreeExchange()
@@ -590,7 +590,7 @@ def update_exchange_rates(force: bool = False):
         backend.update_rates(base_currency=base)
 
         # Remove any exchange rates which are not in the provided currencies
-        Rate.objects.filter(backend="InvenTreeExchange").exclude(
+        Rate.objects.filter(backend='InvenTreeExchange').exclude(
             currency__in=currency_codes()
         ).delete()
 
@@ -598,9 +598,9 @@ def update_exchange_rates(force: bool = False):
         record_task_success('update_exchange_rates')
 
     except (AppRegistryNotReady, OperationalError, ProgrammingError):
-        logger.warning("Could not update exchange rates - database not ready")
+        logger.warning('Could not update exchange rates - database not ready')
     except Exception as e:  # pragma: no cover
-        logger.exception("Error updating exchange rates: %s", str(type(e)))
+        logger.exception('Error updating exchange rates: %s', str(type(e)))
 
 
 @scheduled_task(ScheduledTask.DAILY)
@@ -620,11 +620,11 @@ def run_backup():
     if not check_daily_holdoff('run_backup', interval):
         return
 
-    logger.info("Performing automated database backup task")
+    logger.info('Performing automated database backup task')
 
-    call_command("dbbackup", noinput=True, clean=True, compress=True, interactive=False)
+    call_command('dbbackup', noinput=True, clean=True, compress=True, interactive=False)
     call_command(
-        "mediabackup", noinput=True, clean=True, compress=True, interactive=False
+        'mediabackup', noinput=True, clean=True, compress=True, interactive=False
     )
 
     # Record that this task was successful
@@ -653,7 +653,7 @@ def check_for_migrations():
         logger.info('There are %s pending migrations', n)
         InvenTreeSetting.set_setting('_PENDING_MIGRATIONS', n, None)
 
-    logger.info("Checking for pending database migrations")
+    logger.info('Checking for pending database migrations')
 
     # Force plugin registry reload
     registry.check_reload()
@@ -671,12 +671,12 @@ def check_for_migrations():
 
     # Test if auto-updates are enabled
     if not get_setting('INVENTREE_AUTO_UPDATE', 'auto_update'):
-        logger.info("Auto-update is disabled - skipping migrations")
+        logger.info('Auto-update is disabled - skipping migrations')
         return
 
     # Log open migrations
     for migration in plan:
-        logger.info("- %s", str(migration[0]))
+        logger.info('- %s', str(migration[0]))
 
     # Set the application to maintenance mode - no access from now on.
     set_maintenance_mode(True)
@@ -694,13 +694,13 @@ def check_for_migrations():
         else:
             set_pending_migrations(0)
 
-            logger.info("Completed %s migrations", n)
+            logger.info('Completed %s migrations', n)
 
     # Make sure we are out of maintenance mode
     if get_maintenance_mode():
-        logger.warning("Maintenance mode was not disabled - forcing it now")
+        logger.warning('Maintenance mode was not disabled - forcing it now')
         set_maintenance_mode(False)
-        logger.info("Manually released maintenance mode")
+        logger.info('Manually released maintenance mode')
 
     # We should be current now - triggering full reload to make sure all models
     # are loaded fully in their new state.
diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py
index 4df2dc6907..72ea009853 100644
--- a/InvenTree/InvenTree/test_api.py
+++ b/InvenTree/InvenTree/test_api.py
@@ -69,11 +69,11 @@ class APITests(InvenTreeAPITestCase):
         """Helper function to use basic auth."""
         # Use basic authentication
 
-        authstring = bytes("{u}:{p}".format(u=self.username, p=self.password), "ascii")
+        authstring = bytes('{u}:{p}'.format(u=self.username, p=self.password), 'ascii')
 
         # Use "basic" auth by default
-        auth = b64encode(authstring).decode("ascii")
-        self.client.credentials(HTTP_AUTHORIZATION="Basic {auth}".format(auth=auth))
+        auth = b64encode(authstring).decode('ascii')
+        self.client.credentials(HTTP_AUTHORIZATION='Basic {auth}'.format(auth=auth))
 
     def tokenAuth(self):
         """Helper function to use token auth."""
@@ -274,7 +274,7 @@ class BulkDeleteTests(InvenTreeAPITestCase):
         )
 
         # DELETE with invalid 'items'
-        response = self.delete(url, {'items': {"hello": "world"}}, expected_code=400)
+        response = self.delete(url, {'items': {'hello': 'world'}}, expected_code=400)
 
         self.assertIn("'items' must be supplied as a list object", str(response.data))
 
diff --git a/InvenTree/InvenTree/test_urls.py b/InvenTree/InvenTree/test_urls.py
index 6229d39a81..d15493a750 100644
--- a/InvenTree/InvenTree/test_urls.py
+++ b/InvenTree/InvenTree/test_urls.py
@@ -67,7 +67,7 @@ class URLTest(TestCase):
         """Search for all instances of {% url %} in supplied template file."""
         urls = []
 
-        pattern = "{% url ['\"]([^'\"]+)['\"]([^%]*)%}"
+        pattern = '{% url [\'"]([^\'"]+)[\'"]([^%]*)%}'
 
         with open(input_file, 'r') as f:
             data = f.read()
@@ -91,16 +91,16 @@ class URLTest(TestCase):
             pk = None
 
         # TODO: Handle reverse lookup of admin URLs!
-        if url.startswith("admin:"):
+        if url.startswith('admin:'):
             return
 
         # TODO can this be more elegant?
-        if url.startswith("account_"):
+        if url.startswith('account_'):
             return
 
         if pk:
             # We will assume that there is at least one item in the database
-            reverse(url, kwargs={"pk": 1})
+            reverse(url, kwargs={'pk': 1})
         else:
             reverse(url)
 
@@ -113,14 +113,14 @@ class URLTest(TestCase):
 
     def test_html_templates(self):
         """Test all HTML templates for broken url tags."""
-        template_files = self.find_files("*.html")
+        template_files = self.find_files('*.html')
 
         for f in template_files:
             self.check_file(f)
 
     def test_js_templates(self):
         """Test all JS templates for broken url tags."""
-        template_files = self.find_files("*.js")
+        template_files = self.find_files('*.js')
 
         for f in template_files:
             self.check_file(f)
diff --git a/InvenTree/InvenTree/test_views.py b/InvenTree/InvenTree/test_views.py
index 6defdd9338..48c8db6924 100644
--- a/InvenTree/InvenTree/test_views.py
+++ b/InvenTree/InvenTree/test_views.py
@@ -23,13 +23,13 @@ class ViewTests(InvenTreeTestCase):
 
     def test_index_redirect(self):
         """Top-level URL should redirect to "index" page."""
-        response = self.client.get("/")
+        response = self.client.get('/')
 
         self.assertEqual(response.status_code, 302)
 
     def get_index_page(self):
         """Retrieve the index page (used for subsequent unit tests)"""
-        response = self.client.get("/index/")
+        response = self.client.get('/index/')
 
         self.assertEqual(response.status_code, 200)
 
@@ -68,8 +68,8 @@ class ViewTests(InvenTreeTestCase):
 
         # Default user has staff access, so all panels will be present
         for panel in user_panels + staff_panels + plugin_panels:
-            self.assertIn(f"select-{panel}", content)
-            self.assertIn(f"panel-{panel}", content)
+            self.assertIn(f'select-{panel}', content)
+            self.assertIn(f'panel-{panel}', content)
 
         # Now create a user who does not have staff access
         pleb_user = get_user_model().objects.create_user(
@@ -93,24 +93,24 @@ class ViewTests(InvenTreeTestCase):
 
         # Normal user still has access to user-specific panels
         for panel in user_panels:
-            self.assertIn(f"select-{panel}", content)
-            self.assertIn(f"panel-{panel}", content)
+            self.assertIn(f'select-{panel}', content)
+            self.assertIn(f'panel-{panel}', content)
 
         # Normal user does NOT have access to global or plugin settings
         for panel in staff_panels + plugin_panels:
-            self.assertNotIn(f"select-{panel}", content)
-            self.assertNotIn(f"panel-{panel}", content)
+            self.assertNotIn(f'select-{panel}', content)
+            self.assertNotIn(f'panel-{panel}', content)
 
     def test_url_login(self):
         """Test logging in via arguments"""
         # Log out
         self.client.logout()
-        response = self.client.get("/index/")
+        response = self.client.get('/index/')
         self.assertEqual(response.status_code, 302)
 
         # Try login with url
         response = self.client.get(
-            f"/accounts/login/?next=/&login={self.username}&password={self.password}"
+            f'/accounts/login/?next=/&login={self.username}&password={self.password}'
         )
         self.assertEqual(response.status_code, 302)
         self.assertEqual(response.url, '/')
diff --git a/InvenTree/InvenTree/tests.py b/InvenTree/InvenTree/tests.py
index 833fd55b4a..2488c07685 100644
--- a/InvenTree/InvenTree/tests.py
+++ b/InvenTree/InvenTree/tests.py
@@ -45,12 +45,12 @@ class ConversionTest(TestCase):
     def test_prefixes(self):
         """Test inputs where prefixes are used"""
         tests = {
-            "3": 3,
-            "3m": 3,
-            "3mm": 0.003,
-            "3k": 3000,
-            "3u": 0.000003,
-            "3 inch": 0.0762,
+            '3': 3,
+            '3m': 3,
+            '3mm': 0.003,
+            '3k': 3000,
+            '3u': 0.000003,
+            '3 inch': 0.0762,
         }
 
         for val, expected in tests.items():
@@ -60,13 +60,13 @@ class ConversionTest(TestCase):
     def test_base_units(self):
         """Test conversion to specified base units"""
         tests = {
-            "3": 3,
-            "3 dozen": 36,
-            "50 dozen kW": 600000,
-            "1 / 10": 0.1,
-            "1/2 kW": 500,
-            "1/2 dozen kW": 6000,
-            "0.005 MW": 5000,
+            '3': 3,
+            '3 dozen': 36,
+            '50 dozen kW': 600000,
+            '1 / 10': 0.1,
+            '1/2 kW': 500,
+            '1/2 dozen kW': 6000,
+            '0.005 MW': 5000,
         }
 
         for val, expected in tests.items():
@@ -173,24 +173,24 @@ class ValidatorTest(TestCase):
 
     def test_overage(self):
         """Test overage validator."""
-        validate_overage("100%")
-        validate_overage("10")
-        validate_overage("45.2 %")
+        validate_overage('100%')
+        validate_overage('10')
+        validate_overage('45.2 %')
 
         with self.assertRaises(django_exceptions.ValidationError):
-            validate_overage("-1")
+            validate_overage('-1')
 
         with self.assertRaises(django_exceptions.ValidationError):
-            validate_overage("-2.04 %")
+            validate_overage('-2.04 %')
 
         with self.assertRaises(django_exceptions.ValidationError):
-            validate_overage("105%")
+            validate_overage('105%')
 
         with self.assertRaises(django_exceptions.ValidationError):
-            validate_overage("xxx %")
+            validate_overage('xxx %')
 
         with self.assertRaises(django_exceptions.ValidationError):
-            validate_overage("aaaa")
+            validate_overage('aaaa')
 
     def test_url_validation(self):
         """Test for AllowedURLValidator"""
@@ -230,7 +230,7 @@ class FormatTest(TestCase):
     def test_parse(self):
         """Tests for the 'parse_format_string' function"""
         # Extract data from a valid format string
-        fmt = "PO-{abc:02f}-{ref:04d}-{date}-???"
+        fmt = 'PO-{abc:02f}-{ref:04d}-{date}-???'
 
         info = InvenTree.format.parse_format_string(fmt)
 
@@ -246,10 +246,10 @@ class FormatTest(TestCase):
     def test_create_regex(self):
         """Test function for creating a regex from a format string"""
         tests = {
-            "PO-123-{ref:04f}": r"^PO\-123\-(?P<ref>.+)$",
-            "{PO}-???-{ref}-{date}-22": r"^(?P<PO>.+)\-...\-(?P<ref>.+)\-(?P<date>.+)\-22$",
-            "ABC-123-###-{ref}": r"^ABC\-123\-\d\d\d\-(?P<ref>.+)$",
-            "ABC-123": r"^ABC\-123$",
+            'PO-123-{ref:04f}': r'^PO\-123\-(?P<ref>.+)$',
+            '{PO}-???-{ref}-{date}-22': r'^(?P<PO>.+)\-...\-(?P<ref>.+)\-(?P<date>.+)\-22$',
+            'ABC-123-###-{ref}': r'^ABC\-123\-\d\d\d\-(?P<ref>.+)$',
+            'ABC-123': r'^ABC\-123$',
         }
 
         for fmt, reg in tests.items():
@@ -259,28 +259,28 @@ class FormatTest(TestCase):
         """Test that string validation works as expected"""
         # These tests should pass
         for value, pattern in {
-            "ABC-hello-123": "???-{q}-###",
-            "BO-1234": "BO-{ref}",
-            "111.222.fred.china": "???.###.{name}.{place}",
-            "PO-1234": "PO-{ref:04d}",
+            'ABC-hello-123': '???-{q}-###',
+            'BO-1234': 'BO-{ref}',
+            '111.222.fred.china': '???.###.{name}.{place}',
+            'PO-1234': 'PO-{ref:04d}',
         }.items():
             self.assertTrue(InvenTree.format.validate_string(value, pattern))
 
         # These tests should fail
         for value, pattern in {
-            "ABC-hello-123": "###-{q}-???",
-            "BO-1234": "BO.{ref}",
-            "BO-####": "BO-{pattern}-{next}",
-            "BO-123d": "BO-{ref:04d}",
+            'ABC-hello-123': '###-{q}-???',
+            'BO-1234': 'BO.{ref}',
+            'BO-####': 'BO-{pattern}-{next}',
+            'BO-123d': 'BO-{ref:04d}',
         }.items():
             self.assertFalse(InvenTree.format.validate_string(value, pattern))
 
     def test_extract_value(self):
         """Test that we can extract named values based on a format string"""
         # Simple tests based on a straight-forward format string
-        fmt = "PO-###-{ref:04d}"
+        fmt = 'PO-###-{ref:04d}'
 
-        tests = {"123": "PO-123-123", "456": "PO-123-456", "789": "PO-123-789"}
+        tests = {'123': 'PO-123-123', '456': 'PO-123-456', '789': 'PO-123-789'}
 
         for k, v in tests.items():
             self.assertEqual(InvenTree.format.extract_named_group('ref', v, fmt), k)
@@ -293,8 +293,8 @@ class FormatTest(TestCase):
                 InvenTree.format.extract_named_group('ref', v, fmt)
 
         # More complex tests
-        fmt = "PO-{date}-{test}-???-{ref}-###"
-        val = "PO-2022-02-01-hello-ABC-12345-222"
+        fmt = 'PO-{date}-{test}-???-{ref}-###'
+        val = 'PO-2022-02-01-hello-ABC-12345-222'
 
         data = {'date': '2022-02-01', 'test': 'hello', 'ref': '12345'}
 
@@ -305,42 +305,42 @@ class FormatTest(TestCase):
 
         # Raises a ValueError as the format string is bad
         with self.assertRaises(ValueError):
-            InvenTree.format.extract_named_group("test", "PO-1234-5", "PO-{test}-{")
+            InvenTree.format.extract_named_group('test', 'PO-1234-5', 'PO-{test}-{')
 
         # Raises a NameError as the named group does not exist in the format string
         with self.assertRaises(NameError):
-            InvenTree.format.extract_named_group("missing", "PO-12345", "PO-{test}")
+            InvenTree.format.extract_named_group('missing', 'PO-12345', 'PO-{test}')
 
         # Raises a ValueError as the value does not match the format string
         with self.assertRaises(ValueError):
-            InvenTree.format.extract_named_group("test", "PO-1234", "PO-{test}-1234")
+            InvenTree.format.extract_named_group('test', 'PO-1234', 'PO-{test}-1234')
 
         with self.assertRaises(ValueError):
-            InvenTree.format.extract_named_group("test", "PO-ABC-xyz", "PO-###-{test}")
+            InvenTree.format.extract_named_group('test', 'PO-ABC-xyz', 'PO-###-{test}')
 
     def test_currency_formatting(self):
         """Test that currency formatting works correctly for multiple currencies"""
 
         test_data = (
-            (Money(3651.285718, "USD"), 4, "$3,651.2857"),  # noqa: E201,E202
-            (Money(487587.849178, "CAD"), 5, "CA$487,587.84918"),  # noqa: E201,E202
-            (Money(0.348102, "EUR"), 1, "€0.3"),  # noqa: E201,E202
-            (Money(0.916530, "GBP"), 1, "£0.9"),  # noqa: E201,E202
-            (Money(61.031024, "JPY"), 3, "Â¥61.031"),  # noqa: E201,E202
-            (Money(49609.694602, "JPY"), 1, "Â¥49,609.7"),  # noqa: E201,E202
-            (Money(155565.264777, "AUD"), 2, "A$155,565.26"),  # noqa: E201,E202
-            (Money(0.820437, "CNY"), 4, "CNÂ¥0.8204"),  # noqa: E201,E202
-            (Money(7587.849178, "EUR"), 0, "€7,588"),  # noqa: E201,E202
-            (Money(0.348102, "GBP"), 3, "£0.348"),  # noqa: E201,E202
-            (Money(0.652923, "CHF"), 0, "CHF1"),  # noqa: E201,E202
-            (Money(0.820437, "CNY"), 1, "CNÂ¥0.8"),  # noqa: E201,E202
-            (Money(98789.5295680, "CHF"), 0, "CHF98,790"),  # noqa: E201,E202
-            (Money(0.585787, "USD"), 1, "$0.6"),  # noqa: E201,E202
-            (Money(0.690541, "CAD"), 3, "CA$0.691"),  # noqa: E201,E202
-            (Money(427.814104, "AUD"), 5, "A$427.81410"),  # noqa: E201,E202
+            (Money(3651.285718, 'USD'), 4, '$3,651.2857'),  # noqa: E201,E202
+            (Money(487587.849178, 'CAD'), 5, 'CA$487,587.84918'),  # noqa: E201,E202
+            (Money(0.348102, 'EUR'), 1, '€0.3'),  # noqa: E201,E202
+            (Money(0.916530, 'GBP'), 1, '£0.9'),  # noqa: E201,E202
+            (Money(61.031024, 'JPY'), 3, 'Â¥61.031'),  # noqa: E201,E202
+            (Money(49609.694602, 'JPY'), 1, 'Â¥49,609.7'),  # noqa: E201,E202
+            (Money(155565.264777, 'AUD'), 2, 'A$155,565.26'),  # noqa: E201,E202
+            (Money(0.820437, 'CNY'), 4, 'CNÂ¥0.8204'),  # noqa: E201,E202
+            (Money(7587.849178, 'EUR'), 0, '€7,588'),  # noqa: E201,E202
+            (Money(0.348102, 'GBP'), 3, '£0.348'),  # noqa: E201,E202
+            (Money(0.652923, 'CHF'), 0, 'CHF1'),  # noqa: E201,E202
+            (Money(0.820437, 'CNY'), 1, 'CNÂ¥0.8'),  # noqa: E201,E202
+            (Money(98789.5295680, 'CHF'), 0, 'CHF98,790'),  # noqa: E201,E202
+            (Money(0.585787, 'USD'), 1, '$0.6'),  # noqa: E201,E202
+            (Money(0.690541, 'CAD'), 3, 'CA$0.691'),  # noqa: E201,E202
+            (Money(427.814104, 'AUD'), 5, 'A$427.81410'),  # noqa: E201,E202
         )
 
-        with self.settings(LANGUAGE_CODE="en-us"):
+        with self.settings(LANGUAGE_CODE='en-us'):
             for value, decimal_places, expected_result in test_data:
                 result = InvenTree.format.format_money(
                     value, decimal_places=decimal_places
@@ -353,22 +353,22 @@ class TestHelpers(TestCase):
 
     def test_absolute_url(self):
         """Test helper function for generating an absolute URL"""
-        base = "https://demo.inventree.org:12345"
+        base = 'https://demo.inventree.org:12345'
 
         InvenTreeSetting.set_setting('INVENTREE_BASE_URL', base, change_user=None)
 
         tests = {
-            "": base,
-            "api/": base + "/api/",
-            "/api/": base + "/api/",
-            "api": base + "/api",
-            "media/label/output/": base + "/media/label/output/",
-            "static/logo.png": base + "/static/logo.png",
-            "https://www.google.com": "https://www.google.com",
-            "https://demo.inventree.org:12345/out.html": "https://demo.inventree.org:12345/out.html",
-            "https://demo.inventree.org/test.html": "https://demo.inventree.org/test.html",
-            "http://www.cwi.nl:80/%7Eguido/Python.html": "http://www.cwi.nl:80/%7Eguido/Python.html",
-            "test.org": base + "/test.org",
+            '': base,
+            'api/': base + '/api/',
+            '/api/': base + '/api/',
+            'api': base + '/api',
+            'media/label/output/': base + '/media/label/output/',
+            'static/logo.png': base + '/static/logo.png',
+            'https://www.google.com': 'https://www.google.com',
+            'https://demo.inventree.org:12345/out.html': 'https://demo.inventree.org:12345/out.html',
+            'https://demo.inventree.org/test.html': 'https://demo.inventree.org/test.html',
+            'http://www.cwi.nl:80/%7Eguido/Python.html': 'http://www.cwi.nl:80/%7Eguido/Python.html',
+            'test.org': base + '/test.org',
         }
 
         for url, expected in tests.items():
@@ -442,7 +442,7 @@ class TestHelpers(TestCase):
     def test_download_image(self):
         """Test function for downloading image from remote URL"""
         # Run check with a sequence of bad URLs
-        for url in ["blog", "htp://test.com/?", "google", "\\invalid-url"]:
+        for url in ['blog', 'htp://test.com/?', 'google', '\\invalid-url']:
             with self.assertRaises(django_exceptions.ValidationError):
                 InvenTree.helpers_model.download_image_from_url(url)
 
@@ -467,7 +467,7 @@ class TestHelpers(TestCase):
                             # Re-throw this error
                             raise exc
                         else:
-                            print("Unexpected error:", type(exc), exc)
+                            print('Unexpected error:', type(exc), exc)
 
                     tries += 1
                     time.sleep(10 * tries)
@@ -480,7 +480,7 @@ class TestHelpers(TestCase):
         # TODO: Re-implement this test when we are happier with the external service
         # dl_helper("https://httpstat.us/200?sleep=5000", requests.exceptions.ReadTimeout, timeout=1)
 
-        large_img = "https://github.com/inventree/InvenTree/raw/master/InvenTree/InvenTree/static/img/paper_splash_large.jpg"
+        large_img = 'https://github.com/inventree/InvenTree/raw/master/InvenTree/InvenTree/static/img/paper_splash_large.jpg'
 
         InvenTreeSetting.set_setting(
             'INVENTREE_DOWNLOAD_IMAGE_MAX_SIZE', 1, change_user=None
@@ -527,14 +527,14 @@ class TestIncrement(TestCase):
     def tests(self):
         """Test 'intelligent' incrementing function."""
         tests = [
-            ("", '1'),
-            (1, "2"),
-            ("001", "002"),
-            ("1001", "1002"),
-            ("ABC123", "ABC124"),
-            ("XYZ0", "XYZ1"),
-            ("123Q", "123Q"),
-            ("QQQ", "QQQ"),
+            ('', '1'),
+            (1, '2'),
+            ('001', '002'),
+            ('1001', '1002'),
+            ('ABC123', 'ABC124'),
+            ('XYZ0', 'XYZ1'),
+            ('123Q', '123Q'),
+            ('QQQ', 'QQQ'),
         ]
 
         for test in tests:
@@ -550,7 +550,7 @@ class TestMakeBarcode(TestCase):
     def test_barcode_extended(self):
         """Test creation of barcode with extended data."""
         bc = helpers.MakeBarcode(
-            "part", 3, {"id": 3, "url": "www.google.com"}, brief=False
+            'part', 3, {'id': 3, 'url': 'www.google.com'}, brief=False
         )
 
         self.assertIn('part', bc)
@@ -564,7 +564,7 @@ class TestMakeBarcode(TestCase):
 
     def test_barcode_brief(self):
         """Test creation of simple barcode."""
-        bc = helpers.MakeBarcode("stockitem", 7)
+        bc = helpers.MakeBarcode('stockitem', 7)
 
         data = json.loads(bc)
         self.assertEqual(len(data), 1)
@@ -576,8 +576,8 @@ class TestDownloadFile(TestCase):
 
     def test_download(self):
         """Tests for DownloadFile."""
-        helpers.DownloadFile("hello world", "out.txt")
-        helpers.DownloadFile(bytes(b"hello world"), "out.bin")
+        helpers.DownloadFile('hello world', 'out.txt')
+        helpers.DownloadFile(bytes(b'hello world'), 'out.bin')
 
 
 class TestMPTT(TestCase):
@@ -636,62 +636,62 @@ class TestSerialNumberExtraction(TestCase):
         e = helpers.extract_serial_numbers
 
         # Test a range of numbers
-        sn = e("1-5", 5, 1)
+        sn = e('1-5', 5, 1)
         self.assertEqual(len(sn), 5)
         for i in range(1, 6):
             self.assertIn(str(i), sn)
 
-        sn = e("11-30", 20, 1)
+        sn = e('11-30', 20, 1)
         self.assertEqual(len(sn), 20)
 
-        sn = e("1, 2, 3, 4, 5", 5, 1)
+        sn = e('1, 2, 3, 4, 5', 5, 1)
         self.assertEqual(len(sn), 5)
 
         # Test partially specifying serials
-        sn = e("1, 2, 4+", 5, 1)
+        sn = e('1, 2, 4+', 5, 1)
         self.assertEqual(len(sn), 5)
         self.assertEqual(sn, ['1', '2', '4', '5', '6'])
 
         # Test groups are not interpolated if enough serials are supplied
-        sn = e("1, 2, 3, AF5-69H, 5", 5, 1)
+        sn = e('1, 2, 3, AF5-69H, 5', 5, 1)
         self.assertEqual(len(sn), 5)
         self.assertEqual(sn, ['1', '2', '3', 'AF5-69H', '5'])
 
         # Test groups are not interpolated with more than one hyphen in a word
-        sn = e("1, 2, TG-4SR-92, 4+", 5, 1)
+        sn = e('1, 2, TG-4SR-92, 4+', 5, 1)
         self.assertEqual(len(sn), 5)
-        self.assertEqual(sn, ['1', '2', "TG-4SR-92", '4', '5'])
+        self.assertEqual(sn, ['1', '2', 'TG-4SR-92', '4', '5'])
 
         # Test multiple placeholders
-        sn = e("1 2 ~ ~ ~", 5, 2)
+        sn = e('1 2 ~ ~ ~', 5, 2)
         self.assertEqual(len(sn), 5)
         self.assertEqual(sn, ['1', '2', '3', '4', '5'])
 
-        sn = e("1-5, 10-15", 11, 1)
+        sn = e('1-5, 10-15', 11, 1)
         self.assertIn('3', sn)
         self.assertIn('13', sn)
 
-        sn = e("1+", 10, 1)
+        sn = e('1+', 10, 1)
         self.assertEqual(len(sn), 10)
         self.assertEqual(sn, [str(_) for _ in range(1, 11)])
 
-        sn = e("4, 1+2", 4, 1)
+        sn = e('4, 1+2', 4, 1)
         self.assertEqual(len(sn), 4)
         self.assertEqual(sn, ['4', '1', '2', '3'])
 
-        sn = e("~", 1, 1)
+        sn = e('~', 1, 1)
         self.assertEqual(len(sn), 1)
         self.assertEqual(sn, ['2'])
 
-        sn = e("~", 1, 3)
+        sn = e('~', 1, 3)
         self.assertEqual(len(sn), 1)
         self.assertEqual(sn, ['4'])
 
-        sn = e("~+", 2, 4)
+        sn = e('~+', 2, 4)
         self.assertEqual(len(sn), 2)
         self.assertEqual(sn, ['5', '6'])
 
-        sn = e("~+3", 4, 4)
+        sn = e('~+3', 4, 4)
         self.assertEqual(len(sn), 4)
         self.assertEqual(sn, ['5', '6', '7', '8'])
 
@@ -701,70 +701,70 @@ class TestSerialNumberExtraction(TestCase):
 
         # Test duplicates
         with self.assertRaises(ValidationError):
-            e("1,2,3,3,3", 5, 1)
+            e('1,2,3,3,3', 5, 1)
 
         # Test invalid length
         with self.assertRaises(ValidationError):
-            e("1,2,3", 5, 1)
+            e('1,2,3', 5, 1)
 
         # Test empty string
         with self.assertRaises(ValidationError):
-            e(", , ,", 0, 1)
+            e(', , ,', 0, 1)
 
         # Test incorrect sign in group
         with self.assertRaises(ValidationError):
-            e("10-2", 8, 1)
+            e('10-2', 8, 1)
 
         # Test invalid group
         with self.assertRaises(ValidationError):
-            e("1-5-10", 10, 1)
+            e('1-5-10', 10, 1)
 
         with self.assertRaises(ValidationError):
-            e("10, a, 7-70j", 4, 1)
+            e('10, a, 7-70j', 4, 1)
 
         # Test groups are not interpolated with word characters
         with self.assertRaises(ValidationError):
-            e("1, 2, 3, E-5", 5, 1)
+            e('1, 2, 3, E-5', 5, 1)
 
         # Extract a range of values with a smaller range
         with self.assertRaises(ValidationError) as exc:
-            e("11-50", 10, 1)
+            e('11-50', 10, 1)
             self.assertIn('Range quantity exceeds 10', str(exc))
 
         # Test groups are not interpolated with alpha characters
         with self.assertRaises(ValidationError) as exc:
-            e("1, A-2, 3+", 5, 1)
+            e('1, A-2, 3+', 5, 1)
             self.assertIn('Invalid group range: A-2', str(exc))
 
     def test_combinations(self):
         """Test complex serial number combinations."""
         e = helpers.extract_serial_numbers
 
-        sn = e("1 3-5 9+2", 7, 1)
+        sn = e('1 3-5 9+2', 7, 1)
         self.assertEqual(len(sn), 7)
         self.assertEqual(sn, ['1', '3', '4', '5', '9', '10', '11'])
 
-        sn = e("1,3-5,9+2", 7, 1)
+        sn = e('1,3-5,9+2', 7, 1)
         self.assertEqual(len(sn), 7)
         self.assertEqual(sn, ['1', '3', '4', '5', '9', '10', '11'])
 
-        sn = e("~+2", 3, 13)
+        sn = e('~+2', 3, 13)
         self.assertEqual(len(sn), 3)
         self.assertEqual(sn, ['14', '15', '16'])
 
-        sn = e("~+", 2, 13)
+        sn = e('~+', 2, 13)
         self.assertEqual(len(sn), 2)
         self.assertEqual(sn, ['14', '15'])
 
         # Test multiple increment groups
-        sn = e("~+4, 20+4, 30+4", 15, 10)
+        sn = e('~+4, 20+4, 30+4', 15, 10)
         self.assertEqual(len(sn), 15)
 
         for v in [14, 24, 34]:
             self.assertIn(str(v), sn)
 
         # Test multiple range groups
-        sn = e("11-20, 41-50, 91-100", 30, 1)
+        sn = e('11-20, 41-50, 91-100', 30, 1)
         self.assertEqual(len(sn), 30)
 
         for v in range(11, 21):
@@ -859,7 +859,7 @@ class CurrencyTests(TestCase):
                 break
 
             else:  # pragma: no cover
-                print("Exchange rate update failed - retrying")
+                print('Exchange rate update failed - retrying')
                 print(f'Expected {currency_codes()}, got {[a.currency for a in rates]}')
                 time.sleep(1)
 
@@ -1030,7 +1030,7 @@ class TestSettings(InvenTreeTestCase):
         # test typecasting to dict - valid JSON string should be mapped to corresponding dict
         with self.in_env_context({TEST_ENV_NAME: '{"a": 1}'}):
             self.assertEqual(
-                config.get_setting(TEST_ENV_NAME, None, typecast=dict), {"a": 1}
+                config.get_setting(TEST_ENV_NAME, None, typecast=dict), {'a': 1}
             )
 
         # test typecasting to dict - invalid JSON string should be mapped to empty dict
@@ -1047,8 +1047,8 @@ class TestInstanceName(InvenTreeTestCase):
         self.assertEqual(version.inventreeInstanceTitle(), 'InvenTree')
 
         # set up required setting
-        InvenTreeSetting.set_setting("INVENTREE_INSTANCE_TITLE", True, self.user)
-        InvenTreeSetting.set_setting("INVENTREE_INSTANCE", "Testing title", self.user)
+        InvenTreeSetting.set_setting('INVENTREE_INSTANCE_TITLE', True, self.user)
+        InvenTreeSetting.set_setting('INVENTREE_INSTANCE', 'Testing title', self.user)
 
         self.assertEqual(version.inventreeInstanceTitle(), 'Testing title')
 
@@ -1060,7 +1060,7 @@ class TestInstanceName(InvenTreeTestCase):
         """Test instance url settings."""
         # Set up required setting
         InvenTreeSetting.set_setting(
-            "INVENTREE_BASE_URL", "http://127.1.2.3", self.user
+            'INVENTREE_BASE_URL', 'http://127.1.2.3', self.user
         )
 
         # The site should also be changed
@@ -1107,7 +1107,7 @@ class TestOffloadTask(InvenTreeTestCase):
                 offload_task('dummy_task.numbers', 1, 1, 1, force_sync=True)
             )
 
-            self.assertIn("Malformed function path", str(log.output))
+            self.assertIn('Malformed function path', str(log.output))
 
         # Offload dummy task with a Part instance
         # This should succeed, ensuring that the Part instance is correctly pickled
diff --git a/InvenTree/InvenTree/unit_test.py b/InvenTree/InvenTree/unit_test.py
index 52e84bb589..85af6d9dd2 100644
--- a/InvenTree/InvenTree/unit_test.py
+++ b/InvenTree/InvenTree/unit_test.py
@@ -39,7 +39,7 @@ def getMigrationFileNames(app):
     files = local_dir.joinpath('..', app, 'migrations').iterdir()
 
     # Regex pattern for migration files
-    regex = re.compile(r"^[\d]+_.*\.py$")
+    regex = re.compile(r'^[\d]+_.*\.py$')
 
     migration_files = []
 
@@ -241,14 +241,14 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
             yield  # your test will be run here
 
         if verbose:
-            msg = "\r\n%s" % json.dumps(context.captured_queries, indent=4)
+            msg = '\r\n%s' % json.dumps(context.captured_queries, indent=4)
         else:
             msg = None
 
         n = len(context.captured_queries)
 
         if debug:
-            print(f"Expected less than {value} queries, got {n} queries")
+            print(f'Expected less than {value} queries, got {n} queries')
 
         self.assertLess(n, value, msg=msg)
 
@@ -357,7 +357,7 @@ class InvenTreeAPITestCase(ExchangeRateMixin, UserMixin, APITestCase):
         # Check that the response is of the correct type
         if not isinstance(response, StreamingHttpResponse):
             raise ValueError(
-                "Response is not a StreamingHttpResponse object as expected"
+                'Response is not a StreamingHttpResponse object as expected'
             )
 
         # Extract filename
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index 3f5f5179a5..be6f6be6f7 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -67,7 +67,7 @@ from .views import (
     auth_request,
 )
 
-admin.site.site_header = "InvenTree Admin"
+admin.site.site_header = 'InvenTree Admin'
 
 
 apipatterns = [
@@ -96,7 +96,7 @@ apipatterns = [
     ),
     # InvenTree information endpoints
     path(
-        "version-text", VersionTextView.as_view(), name="api-version-text"
+        'version-text', VersionTextView.as_view(), name='api-version-text'
     ),  # version text
     path('version/', VersionView.as_view(), name='api-version'),  # version info
     path('', InfoView.as_view(), name='api-inventree-info'),  # server info
@@ -153,11 +153,11 @@ apipatterns = [
     ),
     # Magic login URLs
     path(
-        "email/generate/",
+        'email/generate/',
         csrf_exempt(GetSimpleLoginView().as_view()),
-        name="sesame-generate",
+        name='sesame-generate',
     ),
-    path("email/login/", LoginView.as_view(), name="sesame-login"),
+    path('email/login/', LoginView.as_view(), name='sesame-login'),
     # Unknown endpoint
     re_path(r'^.*$', NotFoundView.as_view(), name='api-404'),
 ]
@@ -403,12 +403,12 @@ classic_frontendpatterns = [
         name='socialaccount_connections',
     ),
     re_path(
-        r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$",
+        r'^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$',
         CustomPasswordResetFromKeyView.as_view(),
-        name="account_reset_password_from_key",
+        name='account_reset_password_from_key',
     ),
     # Override login page
-    re_path("accounts/login/", CustomLoginView.as_view(), name="account_login"),
+    re_path('accounts/login/', CustomLoginView.as_view(), name='account_login'),
     re_path(r'^accounts/', include('allauth_2fa.urls')),  # MFA support
     re_path(r'^accounts/', include('allauth.urls')),  # included urlpatterns
 ]
diff --git a/InvenTree/InvenTree/validators.py b/InvenTree/InvenTree/validators.py
index bec31d2f09..74f44dc9b2 100644
--- a/InvenTree/InvenTree/validators.py
+++ b/InvenTree/InvenTree/validators.py
@@ -119,7 +119,7 @@ def validate_overage(value):
         i = Decimal(value)
 
         if i < 0:
-            raise ValidationError(_("Overage value must not be negative"))
+            raise ValidationError(_('Overage value must not be negative'))
 
         # Looks like a number
         return True
@@ -135,15 +135,15 @@ def validate_overage(value):
             f = float(v)
 
             if f < 0:
-                raise ValidationError(_("Overage value must not be negative"))
+                raise ValidationError(_('Overage value must not be negative'))
             elif f > 100:
-                raise ValidationError(_("Overage must not exceed 100%"))
+                raise ValidationError(_('Overage must not exceed 100%'))
 
             return True
         except ValueError:
             pass
 
-    raise ValidationError(_("Invalid value for overage"))
+    raise ValidationError(_('Invalid value for overage'))
 
 
 def validate_part_name_format(value):
diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py
index 997b4fdddd..869c9c8e99 100644
--- a/InvenTree/InvenTree/version.py
+++ b/InvenTree/InvenTree/version.py
@@ -19,7 +19,7 @@ from dulwich.repo import NotGitRepository, Repo
 from .api_version import INVENTREE_API_TEXT, INVENTREE_API_VERSION
 
 # InvenTree software version
-INVENTREE_SW_VERSION = "0.14.0 dev"
+INVENTREE_SW_VERSION = '0.14.0 dev'
 
 # Discover git
 try:
@@ -32,8 +32,8 @@ except (NotGitRepository, FileNotFoundError):
 def checkMinPythonVersion():
     """Check that the Python version is at least 3.9"""
 
-    version = sys.version.split(" ")[0]
-    docs = "https://docs.inventree.org/en/stable/start/intro/#python-requirements"
+    version = sys.version.split(' ')[0]
+    docs = 'https://docs.inventree.org/en/stable/start/intro/#python-requirements'
 
     msg = f"""
     InvenTree requires Python 3.9 or above - you are running version {version}.
@@ -47,22 +47,22 @@ def checkMinPythonVersion():
     if sys.version_info.major == 3 and sys.version_info.minor < 9:
         raise RuntimeError(msg)
 
-    print(f"Python version {version} - {sys.executable}")
+    print(f'Python version {version} - {sys.executable}')
 
 
 def inventreeInstanceName():
     """Returns the InstanceName settings for the current database."""
     import common.models
 
-    return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
+    return common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE', '')
 
 
 def inventreeInstanceTitle():
     """Returns the InstanceTitle for the current database."""
     import common.models
 
-    if common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE_TITLE", False):
-        return common.models.InvenTreeSetting.get_setting("INVENTREE_INSTANCE", "")
+    if common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE_TITLE', False):
+        return common.models.InvenTreeSetting.get_setting('INVENTREE_INSTANCE', '')
     return 'InvenTree'
 
 
@@ -76,7 +76,7 @@ def inventreeVersionTuple(version=None):
     if version is None:
         version = INVENTREE_SW_VERSION
 
-    match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", str(version))
+    match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', str(version))
 
     return [int(g) for g in match.groups()]
 
@@ -93,14 +93,14 @@ def inventreeDocsVersion():
     Release -> "major.minor.sub" e.g. "0.5.2"
     """
     if isInvenTreeDevelopmentVersion():
-        return "latest"
+        return 'latest'
     return INVENTREE_SW_VERSION  # pragma: no cover
 
 
 def inventreeDocUrl():
     """Return URL for InvenTree documentation site."""
     tag = inventreeDocsVersion()
-    return f"https://docs.inventree.org/en/{tag}"
+    return f'https://docs.inventree.org/en/{tag}'
 
 
 def inventreeAppUrl():
@@ -110,12 +110,12 @@ def inventreeAppUrl():
 
 def inventreeCreditsUrl():
     """Return URL for InvenTree credits site."""
-    return "https://docs.inventree.org/en/latest/credits/"
+    return 'https://docs.inventree.org/en/latest/credits/'
 
 
 def inventreeGithubUrl():
     """Return URL for InvenTree github site."""
-    return "https://github.com/InvenTree/InvenTree/"
+    return 'https://github.com/InvenTree/InvenTree/'
 
 
 def isInvenTreeUpToDate():
@@ -147,26 +147,26 @@ def inventreeApiVersion():
 
 def parse_version_text():
     """Parse the version text to structured data."""
-    patched_data = INVENTREE_API_TEXT.split("\n\n")
+    patched_data = INVENTREE_API_TEXT.split('\n\n')
     # Remove first newline on latest version
-    patched_data[0] = patched_data[0].replace("\n", "", 1)
+    patched_data[0] = patched_data[0].replace('\n', '', 1)
 
     version_data = {}
     for version in patched_data:
-        data = version.split("\n")
+        data = version.split('\n')
 
         version_split = data[0].split(' -> ')
         version_detail = (
             version_split[1].split(':', 1) if len(version_split) > 1 else ['']
         )
         new_data = {
-            "version": version_split[0].strip(),
-            "date": version_detail[0].strip(),
-            "gh": version_detail[1].strip() if len(version_detail) > 1 else None,
-            "text": data[1:],
-            "latest": False,
+            'version': version_split[0].strip(),
+            'date': version_detail[0].strip(),
+            'gh': version_detail[1].strip() if len(version_detail) > 1 else None,
+            'text': data[1:],
+            'latest': False,
         }
-        version_data[new_data["version"]] = new_data
+        version_data[new_data['version']] = new_data
     return version_data
 
 
@@ -188,7 +188,7 @@ def inventreeApiText(versions: int = 10, start_version: int = 0):
         start_version = INVENTREE_API_VERSION - versions
 
     return {
-        f"v{a}": version_data.get(f"v{a}", None)
+        f'v{a}': version_data.get(f'v{a}', None)
         for a in range(start_version, start_version + versions)
     }
 
diff --git a/InvenTree/InvenTree/views.py b/InvenTree/InvenTree/views.py
index 7c7767c763..a4e318591e 100644
--- a/InvenTree/InvenTree/views.py
+++ b/InvenTree/InvenTree/views.py
@@ -135,13 +135,13 @@ class InvenTreeRoleMixin(PermissionRequiredMixin):
             app_label = model._meta.app_label
             model_name = model._meta.model_name
 
-            table = f"{app_label}_{model_name}"
+            table = f'{app_label}_{model_name}'
 
             permission = self.get_permission_class()
 
             if not permission:
                 raise AttributeError(
-                    f"permission_class not defined for {type(self).__name__}"
+                    f'permission_class not defined for {type(self).__name__}'
                 )
 
             # Check if the user has the required permission
@@ -396,8 +396,8 @@ class AjaxUpdateView(AjaxMixin, UpdateView):
 class EditUserView(AjaxUpdateView):
     """View for editing user information."""
 
-    ajax_template_name = "modal_form.html"
-    ajax_form_title = _("Edit User Information")
+    ajax_template_name = 'modal_form.html'
+    ajax_form_title = _('Edit User Information')
     form_class = EditUserForm
 
     def get_object(self):
@@ -408,8 +408,8 @@ class EditUserView(AjaxUpdateView):
 class SetPasswordView(AjaxUpdateView):
     """View for setting user password."""
 
-    ajax_template_name = "InvenTree/password.html"
-    ajax_form_title = _("Set Password")
+    ajax_template_name = 'InvenTree/password.html'
+    ajax_form_title = _('Set Password')
     form_class = SetPasswordForm
 
     def get_object(self):
@@ -491,14 +491,14 @@ class SearchView(TemplateView):
 class DynamicJsView(TemplateView):
     """View for returning javacsript files, which instead of being served dynamically, are passed through the django translation engine!"""
 
-    template_name = ""
+    template_name = ''
     content_type = 'text/javascript'
 
 
 class SettingsView(TemplateView):
     """View for configuring User settings."""
 
-    template_name = "InvenTree/settings/settings.html"
+    template_name = 'InvenTree/settings/settings.html'
 
     def get_context_data(self, **kwargs):
         """Add data for template."""
@@ -506,12 +506,12 @@ class SettingsView(TemplateView):
 
         ctx['settings'] = common_models.InvenTreeSetting.objects.all().order_by('key')
 
-        ctx["base_currency"] = common_settings.currency_code_default()
-        ctx["currencies"] = common_settings.currency_codes
+        ctx['base_currency'] = common_settings.currency_code_default()
+        ctx['currencies'] = common_settings.currency_codes
 
-        ctx["rates"] = Rate.objects.filter(backend="InvenTreeExchange")
+        ctx['rates'] = Rate.objects.filter(backend='InvenTreeExchange')
 
-        ctx["categories"] = PartCategory.objects.all().order_by(
+        ctx['categories'] = PartCategory.objects.all().order_by(
             'tree_id', 'lft', 'name'
         )
 
@@ -520,16 +520,16 @@ class SettingsView(TemplateView):
             backend = ExchangeBackend.objects.filter(name='InvenTreeExchange')
             if backend.exists():
                 backend = backend.first()
-                ctx["rates_updated"] = backend.last_update
+                ctx['rates_updated'] = backend.last_update
         except Exception:
-            ctx["rates_updated"] = None
+            ctx['rates_updated'] = None
 
         # Forms and context for allauth
         ctx['add_email_form'] = AddEmailForm
-        ctx["can_add_email"] = EmailAddress.objects.can_add_email(self.request.user)
+        ctx['can_add_email'] = EmailAddress.objects.can_add_email(self.request.user)
 
         # Form and context for allauth social-accounts
-        ctx["request"] = self.request
+        ctx['request'] = self.request
         ctx['social_form'] = DisconnectForm(request=self.request)
 
         # user db sessions
@@ -552,19 +552,19 @@ class AllauthOverrides(LoginRequiredMixin):
 class CustomEmailView(AllauthOverrides, EmailView):
     """Override of allauths EmailView to always show the settings but leave the functions allow."""
 
-    success_url = reverse_lazy("settings")
+    success_url = reverse_lazy('settings')
 
 
 class CustomConnectionsView(AllauthOverrides, ConnectionsView):
     """Override of allauths ConnectionsView to always show the settings but leave the functions allow."""
 
-    success_url = reverse_lazy("settings")
+    success_url = reverse_lazy('settings')
 
 
 class CustomPasswordResetFromKeyView(PasswordResetFromKeyView):
     """Override of allauths PasswordResetFromKeyView to always show the settings but leave the functions allow."""
 
-    success_url = reverse_lazy("account_login")
+    success_url = reverse_lazy('account_login')
 
 
 class UserSessionOverride:
@@ -646,18 +646,18 @@ class AppearanceSelectView(RedirectView):
 class DatabaseStatsView(AjaxView):
     """View for displaying database statistics."""
 
-    ajax_template_name = "stats.html"
-    ajax_form_title = _("System Information")
+    ajax_template_name = 'stats.html'
+    ajax_form_title = _('System Information')
 
 
 class AboutView(AjaxView):
     """A view for displaying InvenTree version information"""
 
-    ajax_template_name = "about.html"
-    ajax_form_title = _("About InvenTree")
+    ajax_template_name = 'about.html'
+    ajax_form_title = _('About InvenTree')
 
 
 class NotificationsView(TemplateView):
     """View for showing notifications."""
 
-    template_name = "InvenTree/notifications/notifications.html"
+    template_name = 'InvenTree/notifications/notifications.html'
diff --git a/InvenTree/InvenTree/wsgi.py b/InvenTree/InvenTree/wsgi.py
index 2a20d06c0d..4630c31182 100644
--- a/InvenTree/InvenTree/wsgi.py
+++ b/InvenTree/InvenTree/wsgi.py
@@ -11,7 +11,7 @@ import os  # pragma: no cover
 from django.core.wsgi import get_wsgi_application  # pragma: no cover
 
 os.environ.setdefault(
-    "DJANGO_SETTINGS_MODULE", "InvenTree.settings"
+    'DJANGO_SETTINGS_MODULE', 'InvenTree.settings'
 )  # pragma: no cover
 
 application = get_wsgi_application()  # pragma: no cover
diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py
index 6187772569..9ccfc4b9d2 100644
--- a/InvenTree/common/api.py
+++ b/InvenTree/common/api.py
@@ -181,7 +181,7 @@ class SettingsList(ListAPI):
 class GlobalSettingsList(SettingsList):
     """API endpoint for accessing a list of global settings objects."""
 
-    queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith="_")
+    queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
     serializer_class = common.serializers.GlobalSettingsSerializer
 
     def list(self, request, *args, **kwargs):
@@ -214,7 +214,7 @@ class GlobalSettingsDetail(RetrieveUpdateAPI):
     """
 
     lookup_field = 'key'
-    queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith="_")
+    queryset = common.models.InvenTreeSetting.objects.exclude(key__startswith='_')
     serializer_class = common.serializers.GlobalSettingsSerializer
 
     def get_object(self):
diff --git a/InvenTree/common/apps.py b/InvenTree/common/apps.py
index f05808a4d8..94e88f6159 100644
--- a/InvenTree/common/apps.py
+++ b/InvenTree/common/apps.py
@@ -29,7 +29,7 @@ class CommonConfig(AppConfig):
             if common.models.InvenTreeSetting.get_setting(
                 'SERVER_RESTART_REQUIRED', backup_value=False, create=False, cache=False
             ):
-                logger.info("Clearing SERVER_RESTART_REQUIRED flag")
+                logger.info('Clearing SERVER_RESTART_REQUIRED flag')
 
                 if not isImportingData():
                     common.models.InvenTreeSetting.set_setting(
diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py
index 076e11cade..aa1e19b9ed 100644
--- a/InvenTree/common/models.py
+++ b/InvenTree/common/models.py
@@ -220,7 +220,7 @@ class BaseInvenTreeSetting(models.Model):
 
         If a particular setting is not present, create it with the default value
         """
-        cache_key = f"BUILD_DEFAULT_VALUES:{str(cls.__name__)}"
+        cache_key = f'BUILD_DEFAULT_VALUES:{str(cls.__name__)}'
 
         if InvenTree.helpers.str2bool(cache.get(cache_key, False)):
             # Already built default values
@@ -234,7 +234,7 @@ class BaseInvenTreeSetting(models.Model):
 
             if len(missing_keys) > 0:
                 logger.info(
-                    "Building %s default values for %s", len(missing_keys), str(cls)
+                    'Building %s default values for %s', len(missing_keys), str(cls)
                 )
                 cls.objects.bulk_create([
                     cls(key=key, value=cls.get_setting_default(key), **kwargs)
@@ -243,7 +243,7 @@ class BaseInvenTreeSetting(models.Model):
                 ])
         except Exception as exc:
             logger.exception(
-                "Failed to build default values for %s (%s)", str(cls), str(type(exc))
+                'Failed to build default values for %s (%s)', str(cls), str(type(exc))
             )
             pass
 
@@ -299,12 +299,12 @@ class BaseInvenTreeSetting(models.Model):
         - The unique KEY string
         - Any key:value kwargs associated with the particular setting type (e.g. user-id)
         """
-        key = f"{str(cls.__name__)}:{setting_key}"
+        key = f'{str(cls.__name__)}:{setting_key}'
 
         for k, v in kwargs.items():
-            key += f"_{k}:{v}"
+            key += f'_{k}:{v}'
 
-        return key.replace(" ", "")
+        return key.replace(' ', '')
 
     @classmethod
     def get_filters(cls, **kwargs):
@@ -366,14 +366,14 @@ class BaseInvenTreeSetting(models.Model):
                 )
 
             # remove any hidden settings
-            if exclude_hidden and setting.get("hidden", False):
+            if exclude_hidden and setting.get('hidden', False):
                 del settings[key.upper()]
 
         # format settings values and remove protected
         for key, setting in settings.items():
             validator = cls.get_setting_validator(key, **filters)
 
-            if cls.is_protected(key, **filters) and setting.value != "":
+            if cls.is_protected(key, **filters) and setting.value != '':
                 setting.value = '***'
             elif cls.validator_is_bool(validator):
                 setting.value = InvenTree.helpers.str2bool(setting.value)
@@ -438,7 +438,7 @@ class BaseInvenTreeSetting(models.Model):
             if setting.required:
                 value = setting.value or cls.get_setting_default(setting.key, **kwargs)
 
-                if value == "":
+                if value == '':
                     missing_settings.append(setting.key.upper())
 
         return len(missing_settings) == 0, missing_settings
@@ -766,7 +766,7 @@ class BaseInvenTreeSetting(models.Model):
         options = self.valid_options()
 
         if options and self.value not in options:
-            raise ValidationError(_("Chosen value is not a valid option"))
+            raise ValidationError(_('Chosen value is not a valid option'))
 
     def run_validator(self, validator):
         """Run a validator against the 'value' field for this InvenTreeSetting object."""
@@ -1049,7 +1049,7 @@ class BaseInvenTreeSetting(models.Model):
         """Check if this setting value is required."""
         setting = cls.get_setting_definition(key, **cls.get_filters(**kwargs))
 
-        return setting.get("required", False)
+        return setting.get('required', False)
 
     @property
     def required(self):
@@ -1131,8 +1131,8 @@ class InvenTreeSetting(BaseInvenTreeSetting):
     class Meta:
         """Meta options for InvenTreeSetting."""
 
-        verbose_name = "InvenTree Setting"
-        verbose_name_plural = "InvenTree Settings"
+        verbose_name = 'InvenTree Setting'
+        verbose_name_plural = 'InvenTree Settings'
 
     def save(self, *args, **kwargs):
         """When saving a global setting, check to see if it requires a server restart.
@@ -1454,7 +1454,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
             'name': _('Part Name Display Format'),
             'description': _('Format to display the part name'),
             'default': "{{ part.IPN if part.IPN }}{{ ' | ' if part.IPN }}{{ part.name }}{{ ' | ' if part.revision }}"
-            "{{ part.revision if part.revision }}",
+            '{{ part.revision if part.revision }}',
             'validator': InvenTree.validators.validate_part_name_format,
         },
         'PART_CATEGORY_DEFAULT_ICON': {
@@ -1860,7 +1860,7 @@ class InvenTreeSetting(BaseInvenTreeSetting):
             'validator': bool,
             'after_save': reload_plugin_registry,
         },
-        "PROJECT_CODES_ENABLED": {
+        'PROJECT_CODES_ENABLED': {
             'name': _('Enable project codes'),
             'description': _('Enable project codes for tracking projects'),
             'default': False,
@@ -1946,8 +1946,8 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
     class Meta:
         """Meta options for InvenTreeUserSetting."""
 
-        verbose_name = "InvenTree User Setting"
-        verbose_name_plural = "InvenTree User Settings"
+        verbose_name = 'InvenTree User Setting'
+        verbose_name_plural = 'InvenTree User Settings'
         constraints = [
             models.UniqueConstraint(fields=['key', 'user'], name='unique key and user')
         ]
@@ -2069,7 +2069,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
             'default': False,
             'validator': bool,
         },
-        "LABEL_INLINE": {
+        'LABEL_INLINE': {
             'name': _('Inline label display'),
             'description': _(
                 'Display PDF labels in the browser, instead of downloading as a file'
@@ -2077,7 +2077,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
             'default': True,
             'validator': bool,
         },
-        "LABEL_DEFAULT_PRINTER": {
+        'LABEL_DEFAULT_PRINTER': {
             'name': _('Default label printer'),
             'description': _(
                 'Configure which label printer should be selected by default'
@@ -2085,7 +2085,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
             'default': '',
             'choices': label_printer_options,
         },
-        "REPORT_INLINE": {
+        'REPORT_INLINE': {
             'name': _('Inline report display'),
             'description': _(
                 'Display PDF reports in the browser, instead of downloading as a file'
@@ -2112,7 +2112,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
             'validator': bool,
         },
         'SEARCH_HIDE_INACTIVE_PARTS': {
-            'name': _("Hide Inactive Parts"),
+            'name': _('Hide Inactive Parts'),
             'description': _('Excluded inactive parts from search preview window'),
             'default': False,
             'validator': bool,
@@ -2360,7 +2360,7 @@ class PriceBreak(MetaMixin):
             converted = convert_money(self.price, currency_code)
         except MissingRate:
             logger.warning(
-                "No currency conversion rate available for %s -> %s",
+                'No currency conversion rate available for %s -> %s',
                 self.price_currency,
                 currency_code,
             )
@@ -2510,11 +2510,11 @@ class WebhookEndpoint(models.Model):
     """
 
     # Token
-    TOKEN_NAME = "Token"
+    TOKEN_NAME = 'Token'
     VERIFICATION_METHOD = VerificationMethod.NONE
 
-    MESSAGE_OK = "Message was received."
-    MESSAGE_TOKEN_ERROR = "Incorrect token in header."
+    MESSAGE_OK = 'Message was received.'
+    MESSAGE_TOKEN_ERROR = 'Incorrect token in header.'
 
     endpoint_id = models.CharField(
         max_length=255,
@@ -2589,7 +2589,7 @@ class WebhookEndpoint(models.Model):
 
         This can be overridden to create your own token validation method.
         """
-        token = headers.get(self.TOKEN_NAME, "")
+        token = headers.get(self.TOKEN_NAME, '')
 
         # no token
         if self.verify == VerificationMethod.NONE:
diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py
index 3dfe2fb716..fadcdd7f05 100644
--- a/InvenTree/common/notifications.py
+++ b/InvenTree/common/notifications.py
@@ -311,23 +311,23 @@ class InvenTreeNotificationBodies:
     """
 
     NewOrder = NotificationBody(
-        name=_("New {verbose_name}"),
+        name=_('New {verbose_name}'),
         slug='{app_label}.new_{model_name}',
-        message=_("A new order has been created and assigned to you"),
+        message=_('A new order has been created and assigned to you'),
         template='email/new_order_assigned.html',
     )
     """Send when a new order (build, sale or purchase) was created."""
 
     OrderCanceled = NotificationBody(
-        name=_("{verbose_name} canceled"),
+        name=_('{verbose_name} canceled'),
         slug='{app_label}.canceled_{model_name}',
-        message=_("A order that is assigned to you was canceled"),
+        message=_('A order that is assigned to you was canceled'),
         template='email/canceled_order_assigned.html',
     )
     """Send when a order (sale, return or purchase) was canceled."""
 
     ItemsReceived = NotificationBody(
-        name=_("Items Received"),
+        name=_('Items Received'),
         slug='purchase_order.items_received',
         message=_('Items have been received against a purchase order'),
         template='email/purchase_order_received.html',
@@ -414,7 +414,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
             # Unhandled type
             else:
                 logger.error(
-                    "Unknown target passed to trigger_notification method: %s", target
+                    'Unknown target passed to trigger_notification method: %s', target
                 )
 
     if target_users:
@@ -515,4 +515,4 @@ def deliver_notification(
             str(obj),
         )
         if not success:
-            logger.info("There were some problems")
+            logger.info('There were some problems')
diff --git a/InvenTree/common/tasks.py b/InvenTree/common/tasks.py
index fca5a49f8c..fc20b9dbfe 100644
--- a/InvenTree/common/tasks.py
+++ b/InvenTree/common/tasks.py
@@ -51,7 +51,7 @@ def update_news_feed():
     try:
         d = feedparser.parse(settings.INVENTREE_NEWS_URL)
     except Exception as entry:  # pragma: no cover
-        logger.warning("update_news_feed: Error parsing the newsfeed", entry)
+        logger.warning('update_news_feed: Error parsing the newsfeed', entry)
         return
 
     # Get a reference list
@@ -97,7 +97,7 @@ def delete_old_notes_images():
     # Remove any notes which point to non-existent image files
     for note in NotesImage.objects.all():
         if not os.path.exists(note.image.path):
-            logger.info("Deleting note %s - image file does not exist", note.image.path)
+            logger.info('Deleting note %s - image file does not exist', note.image.path)
             note.delete()
 
     note_classes = getModelsWithMixin(InvenTreeNotesMixin)
@@ -116,7 +116,7 @@ def delete_old_notes_images():
                 break
 
         if not found:
-            logger.info("Deleting note %s - image file not linked to a note", img)
+            logger.info('Deleting note %s - image file not linked to a note', img)
             note.delete()
 
     # Finally, remove any images in the notes dir which are not linked to a note
@@ -139,5 +139,5 @@ def delete_old_notes_images():
                 break
 
         if not found:
-            logger.info("Deleting note %s - image file not linked to a note", image)
+            logger.info('Deleting note %s - image file not linked to a note', image)
             os.remove(os.path.join(notes_dir, image))
diff --git a/InvenTree/common/tests.py b/InvenTree/common/tests.py
index dfd764c7bc..f612e0d5fb 100644
--- a/InvenTree/common/tests.py
+++ b/InvenTree/common/tests.py
@@ -136,19 +136,19 @@ class SettingsTest(InvenTreeTestCase):
     def test_all_settings(self):
         """Make sure that the all_settings function returns correctly"""
         result = InvenTreeSetting.all_settings()
-        self.assertIn("INVENTREE_INSTANCE", result)
+        self.assertIn('INVENTREE_INSTANCE', result)
         self.assertIsInstance(result['INVENTREE_INSTANCE'], InvenTreeSetting)
 
-    @mock.patch("common.models.InvenTreeSetting.get_setting_definition")
+    @mock.patch('common.models.InvenTreeSetting.get_setting_definition')
     def test_check_all_settings(self, get_setting_definition):
         """Make sure that the check_all_settings function returns correctly"""
         # define partial schema
         settings_definition = {
-            "AB": {  # key that's has not already been accessed
-                "required": True
+            'AB': {  # key that's has not already been accessed
+                'required': True
             },
-            "CD": {"required": True, "protected": True},
-            "EF": {},
+            'CD': {'required': True, 'protected': True},
+            'EF': {},
         }
 
         def mocked(key, **kwargs):
@@ -160,28 +160,28 @@ class SettingsTest(InvenTreeTestCase):
             InvenTreeSetting.check_all_settings(
                 settings_definition=settings_definition
             ),
-            (False, ["AB", "CD"]),
+            (False, ['AB', 'CD']),
         )
-        InvenTreeSetting.set_setting('AB', "hello", self.user)
-        InvenTreeSetting.set_setting('CD', "world", self.user)
+        InvenTreeSetting.set_setting('AB', 'hello', self.user)
+        InvenTreeSetting.set_setting('CD', 'world', self.user)
         self.assertEqual(InvenTreeSetting.check_all_settings(), (True, []))
 
-    @mock.patch("common.models.InvenTreeSetting.get_setting_definition")
+    @mock.patch('common.models.InvenTreeSetting.get_setting_definition')
     def test_settings_validator(self, get_setting_definition):
         """Make sure that the validator function gets called on set setting."""
 
         def validator(x):
-            if x == "hello":
+            if x == 'hello':
                 return x
 
-            raise ValidationError(f"{x} is not valid")
+            raise ValidationError(f'{x} is not valid')
 
         mock_validator = mock.Mock(side_effect=validator)
 
         # define partial schema
         settings_definition = {
-            "AB": {  # key that's has not already been accessed
-                "validator": mock_validator
+            'AB': {  # key that's has not already been accessed
+                'validator': mock_validator
             }
         }
 
@@ -190,12 +190,12 @@ class SettingsTest(InvenTreeTestCase):
 
         get_setting_definition.side_effect = mocked
 
-        InvenTreeSetting.set_setting("AB", "hello", self.user)
-        mock_validator.assert_called_with("hello")
+        InvenTreeSetting.set_setting('AB', 'hello', self.user)
+        mock_validator.assert_called_with('hello')
 
         with self.assertRaises(ValidationError):
-            InvenTreeSetting.set_setting("AB", "world", self.user)
-        mock_validator.assert_called_with("world")
+            InvenTreeSetting.set_setting('AB', 'world', self.user)
+        mock_validator.assert_called_with('world')
 
     def run_settings_check(self, key, setting):
         """Test that all settings are valid.
@@ -322,7 +322,7 @@ class SettingsTest(InvenTreeTestCase):
         # Generate a number of new users
         for idx in range(5):
             get_user_model().objects.create(
-                username=f"User_{idx}", password="hunter42", email="email@dot.com"
+                username=f'User_{idx}', password='hunter42', email='email@dot.com'
             )
 
         key = 'SEARCH_PREVIEW_RESULTS'
@@ -333,7 +333,7 @@ class SettingsTest(InvenTreeTestCase):
             cache_key = setting.cache_key
             self.assertEqual(
                 cache_key,
-                f"InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}",
+                f'InvenTreeUserSetting:SEARCH_PREVIEW_RESULTS_user:{user.username}',
             )
             InvenTreeUserSetting.set_setting(key, user.pk, None, user=user)
             self.assertIsNotNone(cache.get(cache_key))
@@ -399,7 +399,7 @@ class GlobalSettingsApiTest(InvenTreeAPITestCase):
     def test_api_detail(self):
         """Test that we can access the detail view for a setting based on the <key>."""
         # These keys are invalid, and should return 404
-        for key in ["apple", "carrot", "dog"]:
+        for key in ['apple', 'carrot', 'dog']:
             response = self.get(
                 reverse('api-global-setting-detail', kwargs={'key': key}),
                 expected_code=404,
@@ -770,7 +770,7 @@ class WebhookMessageTests(TestCase):
         """
         response = self.client.post(
             self.url,
-            data={"this": "is a message"},
+            data={'this': 'is a message'},
             content_type=CONTENT_TYPE_JSON,
             **{'HTTP_TOKEN': str(self.endpoint_def.token)},
         )
@@ -778,7 +778,7 @@ class WebhookMessageTests(TestCase):
         assert response.status_code == HTTPStatus.OK
         assert str(response.content, 'utf-8') == WebhookView.model_class.MESSAGE_OK
         message = WebhookMessage.objects.get()
-        assert message.body == {"this": "is a message"}
+        assert message.body == {'this': 'is a message'}
 
 
 class NotificationTest(InvenTreeAPITestCase):
@@ -1033,7 +1033,7 @@ class CurrencyAPITests(InvenTreeAPITestCase):
             # Delay and try again
             time.sleep(10)
 
-        raise TimeoutError("Could not refresh currency exchange data after 5 attempts")
+        raise TimeoutError('Could not refresh currency exchange data after 5 attempts')
 
 
 class NotesImageTest(InvenTreeAPITestCase):
@@ -1048,28 +1048,28 @@ class NotesImageTest(InvenTreeAPITestCase):
             reverse('api-notes-image-list'),
             data={
                 'image': SimpleUploadedFile(
-                    'test.txt', b"this is not an image file", content_type='text/plain'
+                    'test.txt', b'this is not an image file', content_type='text/plain'
                 )
             },
             format='multipart',
             expected_code=400,
         )
 
-        self.assertIn("Upload a valid image", str(response.data['image']))
+        self.assertIn('Upload a valid image', str(response.data['image']))
 
         # Test upload of an invalid image file
         response = self.post(
             reverse('api-notes-image-list'),
             data={
                 'image': SimpleUploadedFile(
-                    'test.png', b"this is not an image file", content_type='image/png'
+                    'test.png', b'this is not an image file', content_type='image/png'
                 )
             },
             format='multipart',
             expected_code=400,
         )
 
-        self.assertIn("Upload a valid image", str(response.data['image']))
+        self.assertIn('Upload a valid image', str(response.data['image']))
 
         # Check that no extra database entries have been created
         self.assertEqual(NotesImage.objects.count(), n)
diff --git a/InvenTree/common/views.py b/InvenTree/common/views.py
index 79e6d63ddc..3d9cd53c22 100644
--- a/InvenTree/common/views.py
+++ b/InvenTree/common/views.py
@@ -81,7 +81,7 @@ class FileManagementFormView(MultiStepFormView):
         ('fields', forms.MatchFieldForm),
         ('items', forms.MatchItemForm),
     ]
-    form_steps_description = [_("Upload File"), _("Match Fields"), _("Match Items")]
+    form_steps_description = [_('Upload File'), _('Match Fields'), _('Match Items')]
     media_folder = 'file_upload/'
     extra_context_data = {}
 
diff --git a/InvenTree/company/models.py b/InvenTree/company/models.py
index c4e4d1f5cc..9097708697 100644
--- a/InvenTree/company/models.py
+++ b/InvenTree/company/models.py
@@ -96,7 +96,7 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
         constraints = [
             UniqueConstraint(fields=['name', 'email'], name='unique_name_email_pair')
         ]
-        verbose_name_plural = "Companies"
+        verbose_name_plural = 'Companies'
 
     @staticmethod
     def get_api_url():
@@ -215,7 +215,7 @@ class Company(InvenTreeNotesMixin, MetadataMixin, models.Model):
 
     def __str__(self):
         """Get string representation of a Company."""
-        return f"{self.name} - {self.description}"
+        return f'{self.name} - {self.description}'
 
     def get_absolute_url(self):
         """Get the web URL for the detail view for this Company."""
@@ -318,7 +318,7 @@ class Address(models.Model):
     class Meta:
         """Metaclass defines extra model options"""
 
-        verbose_name_plural = "Addresses"
+        verbose_name_plural = 'Addresses'
 
     def __init__(self, *args, **kwargs):
         """Custom init function"""
@@ -340,7 +340,7 @@ class Address(models.Model):
             if len(line) > 0:
                 populated_lines.append(line)
 
-        return ", ".join(populated_lines)
+        return ', '.join(populated_lines)
 
     def save(self, *args, **kwargs):
         """Run checks when saving an address:
@@ -564,7 +564,7 @@ class ManufacturerPartAttachment(InvenTreeAttachment):
 
     def getSubdir(self):
         """Return the subdirectory where attachment files for the ManufacturerPart model are located"""
-        return os.path.join("manufacturer_part_files", str(self.manufacturer_part.id))
+        return os.path.join('manufacturer_part_files', str(self.manufacturer_part.id))
 
     manufacturer_part = models.ForeignKey(
         ManufacturerPart,
@@ -711,14 +711,14 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
                 ):
                     raise ValidationError({
                         'pack_quantity': _(
-                            "Pack units must be compatible with the base part units"
+                            'Pack units must be compatible with the base part units'
                         )
                     })
 
                 # Native value must be greater than zero
                 if float(native_value.magnitude) <= 0:
                     raise ValidationError({
-                        'pack_quantity': _("Pack units must be greater than zero")
+                        'pack_quantity': _('Pack units must be greater than zero')
                     })
 
                 # Update native pack units value
@@ -732,7 +732,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
             if self.manufacturer_part.part != self.part:
                 raise ValidationError({
                     'manufacturer_part': _(
-                        "Linked manufacturer part must reference the same base part"
+                        'Linked manufacturer part must reference the same base part'
                     )
                 })
 
@@ -787,7 +787,7 @@ class SupplierPart(MetadataMixin, InvenTreeBarcodeMixin, common.models.MetaMixin
 
     SKU = models.CharField(
         max_length=100,
-        verbose_name=__("SKU = Stock Keeping Unit (supplier part number)", 'SKU'),
+        verbose_name=__('SKU = Stock Keeping Unit (supplier part number)', 'SKU'),
         help_text=_('Supplier stock keeping unit'),
     )
 
@@ -1007,7 +1007,7 @@ class SupplierPriceBreak(common.models.PriceBreak):
     class Meta:
         """Metaclass defines extra model options"""
 
-        unique_together = ("part", "quantity")
+        unique_together = ('part', 'quantity')
 
         # This model was moved from the 'Part' app
         db_table = 'part_supplierpricebreak'
diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py
index 42d796affd..192187a3b5 100644
--- a/InvenTree/company/serializers.py
+++ b/InvenTree/company/serializers.py
@@ -168,7 +168,7 @@ class CompanySerializer(RemoteImageMixin, InvenTreeModelSerializer):
             remote_img.save(buffer, format=fmt)
 
             # Construct a simplified name for the image
-            filename = f"company_{company.pk}_image.{fmt.lower()}"
+            filename = f'company_{company.pk}_image.{fmt.lower()}'
 
             company.image.save(filename, ContentFile(buffer.getvalue()))
 
diff --git a/InvenTree/company/test_api.py b/InvenTree/company/test_api.py
index 934f9e4bb6..4bcae45bf6 100644
--- a/InvenTree/company/test_api.py
+++ b/InvenTree/company/test_api.py
@@ -107,8 +107,8 @@ class CompanyTest(InvenTreeAPITestCase):
         response = self.post(
             url,
             {
-                'name': "Another Company",
-                'description': "Also created via the API!",
+                'name': 'Another Company',
+                'description': 'Also created via the API!',
                 'currency': 'AUD',
                 'is_supplier': False,
                 'is_manufacturer': True,
@@ -125,7 +125,7 @@ class CompanyTest(InvenTreeAPITestCase):
         # Attempt to create with invalid currency
         response = self.post(
             url,
-            {'name': "A name", 'description': 'A description', 'currency': 'POQD'},
+            {'name': 'A name', 'description': 'A description', 'currency': 'POQD'},
             expected_code=400,
         )
 
@@ -144,7 +144,7 @@ class ContactTest(InvenTreeAPITestCase):
 
         # Create some companies
         companies = [
-            Company(name=f"Company {idx}", description="Some company")
+            Company(name=f'Company {idx}', description='Some company')
             for idx in range(3)
         ]
 
@@ -155,7 +155,7 @@ class ContactTest(InvenTreeAPITestCase):
         # Create some contacts
         for cmp in Company.objects.all():
             contacts += [
-                Contact(company=cmp, name=f"My name {idx}") for idx in range(3)
+                Contact(company=cmp, name=f'My name {idx}') for idx in range(3)
             ]
 
         Contact.objects.bulk_create(contacts)
@@ -251,7 +251,7 @@ class AddressTest(InvenTreeAPITestCase):
         cls.num_addr = 3
         # Create some companies
         companies = [
-            Company(name=f"Company {idx}", description="Some company")
+            Company(name=f'Company {idx}', description='Some company')
             for idx in range(cls.num_companies)
         ]
 
@@ -262,7 +262,7 @@ class AddressTest(InvenTreeAPITestCase):
         # Create some contacts
         for cmp in Company.objects.all():
             addresses += [
-                Address(company=cmp, title=f"Address no. {idx}")
+                Address(company=cmp, title=f'Address no. {idx}')
                 for idx in range(cls.num_addr)
             ]
 
diff --git a/InvenTree/company/test_migrations.py b/InvenTree/company/test_migrations.py
index 22cbd4d0e6..b4b291644a 100644
--- a/InvenTree/company/test_migrations.py
+++ b/InvenTree/company/test_migrations.py
@@ -228,8 +228,8 @@ class TestCurrencyMigration(MigratorTestCase):
         Part = self.old_state.apps.get_model('part', 'part')
 
         part = Part.objects.create(
-            name="PART",
-            description="A purchaseable part",
+            name='PART',
+            description='A purchaseable part',
             purchaseable=True,
             level=0,
             tree_id=0,
@@ -309,7 +309,7 @@ class TestAddressMigration(MigratorTestCase):
         a2 = Address.objects.filter(company=c2.pk).first()
 
         self.assertEqual(a1.line1, self.short_l1)
-        self.assertEqual(a1.line2, "")
+        self.assertEqual(a1.line2, '')
         self.assertEqual(a2.line1, self.long_l1)
         self.assertEqual(a2.line2, self.l2)
         self.assertEqual(c1.address, '')
@@ -329,8 +329,8 @@ class TestSupplierPartQuantity(MigratorTestCase):
         SupplierPart = self.old_state.apps.get_model('company', 'supplierpart')
 
         self.part = Part.objects.create(
-            name="PART",
-            description="A purchaseable part",
+            name='PART',
+            description='A purchaseable part',
             purchaseable=True,
             level=0,
             tree_id=0,
diff --git a/InvenTree/company/tests.py b/InvenTree/company/tests.py
index cbb235c2a4..9eb2190a98 100644
--- a/InvenTree/company/tests.py
+++ b/InvenTree/company/tests.py
@@ -103,9 +103,9 @@ class CompanySimpleTest(TestCase):
         """Unit tests for supplier part pricing"""
         m2x4 = Part.objects.get(name='M2x4 LPHS')
 
-        self.assertEqual(m2x4.get_price_info(5.5), "38.5 - 41.25")
-        self.assertEqual(m2x4.get_price_info(10), "70 - 75")
-        self.assertEqual(m2x4.get_price_info(100), "125 - 350")
+        self.assertEqual(m2x4.get_price_info(5.5), '38.5 - 41.25')
+        self.assertEqual(m2x4.get_price_info(10), '70 - 75')
+        self.assertEqual(m2x4.get_price_info(100), '125 - 350')
 
         pmin, pmax = m2x4.get_price_range(5)
         self.assertEqual(pmin, 35)
@@ -222,13 +222,13 @@ class AddressTest(TestCase):
 
     def test_model_str(self):
         """Test value of __str__"""
-        t = "Test address"
-        l1 = "Busy street 56"
-        l2 = "Red building"
-        pcd = "12345"
-        pct = "City"
-        pv = "Province"
-        cn = "COUNTRY"
+        t = 'Test address'
+        l1 = 'Busy street 56'
+        l2 = 'Red building'
+        pcd = '12345'
+        pct = 'City'
+        pv = 'Province'
+        cn = 'COUNTRY'
         addr = Address.objects.create(
             company=self.c,
             title=t,
diff --git a/InvenTree/generic/states/api.py b/InvenTree/generic/states/api.py
index c5cee43611..1d7d3db59e 100644
--- a/InvenTree/generic/states/api.py
+++ b/InvenTree/generic/states/api.py
@@ -39,10 +39,10 @@ class StatusView(APIView):
         status_class = self.get_status_model()
 
         if not inspect.isclass(status_class):
-            raise NotImplementedError("`status_class` not a class")
+            raise NotImplementedError('`status_class` not a class')
 
         if not issubclass(status_class, StatusCode):
-            raise NotImplementedError("`status_class` not a valid StatusCode class")
+            raise NotImplementedError('`status_class` not a valid StatusCode class')
 
         data = {'class': status_class.__name__, 'values': status_class.dict()}
 
diff --git a/InvenTree/generic/states/tests.py b/InvenTree/generic/states/tests.py
index d20537ecfd..0d5f926f40 100644
--- a/InvenTree/generic/states/tests.py
+++ b/InvenTree/generic/states/tests.py
@@ -14,9 +14,9 @@ from .states import StatusCode
 class GeneralStatus(StatusCode):
     """Defines a set of status codes for tests."""
 
-    PENDING = 10, _("Pending"), 'secondary'
-    PLACED = 20, _("Placed"), 'primary'
-    COMPLETE = 30, _("Complete"), 'success'
+    PENDING = 10, _('Pending'), 'secondary'
+    PLACED = 20, _('Placed'), 'primary'
+    COMPLETE = 30, _('Complete'), 'success'
     ABC = None  # This should be ignored
     _DEF = None  # This should be ignored
     jkl = None  # This should be ignored
@@ -183,11 +183,11 @@ class GeneralStateTest(InvenTreeTestCase):
         # Invalid call - not a class
         with self.assertRaises(NotImplementedError) as e:
             resp = view(rqst, **{StatusView.MODEL_REF: 'invalid'})
-        self.assertEqual(str(e.exception), "`status_class` not a class")
+        self.assertEqual(str(e.exception), '`status_class` not a class')
 
         # Invalid call - not the right class
         with self.assertRaises(NotImplementedError) as e:
             resp = view(rqst, **{StatusView.MODEL_REF: object})
         self.assertEqual(
-            str(e.exception), "`status_class` not a valid StatusCode class"
+            str(e.exception), '`status_class` not a valid StatusCode class'
         )
diff --git a/InvenTree/gunicorn.conf.py b/InvenTree/gunicorn.conf.py
index c1b403de12..5c26c15631 100644
--- a/InvenTree/gunicorn.conf.py
+++ b/InvenTree/gunicorn.conf.py
@@ -2,7 +2,7 @@
 
 import multiprocessing
 
-bind = "0.0.0.0:8000"
+bind = '0.0.0.0:8000'
 
 workers = multiprocessing.cpu_count() * 2 + 1
 
diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py
index f73a316918..92a9034433 100644
--- a/InvenTree/label/api.py
+++ b/InvenTree/label/api.py
@@ -130,12 +130,12 @@ class LabelListView(LabelFilterMixin, ListCreateAPI):
 class LabelPrintMixin(LabelFilterMixin):
     """Mixin for printing labels."""
 
-    rolemap = {"GET": "view", "POST": "view"}
+    rolemap = {'GET': 'view', 'POST': 'view'}
 
     def check_permissions(self, request):
         """Override request method to GET so that also non superusers can print using a post request."""
-        if request.method == "POST":
-            request = clone_request(request, "GET")
+        if request.method == 'POST':
+            request = clone_request(request, 'GET')
         return super().check_permissions(request)
 
     @method_decorator(never_cache)
@@ -199,7 +199,7 @@ class LabelPrintMixin(LabelFilterMixin):
         if not plugin.is_active():
             raise ValidationError(f"Plugin '{plugin_key}' is not enabled")
 
-        if not plugin.mixin_enabled("labels"):
+        if not plugin.mixin_enabled('labels'):
             raise ValidationError(
                 f"Plugin '{plugin_key}' is not a label printing plugin"
             )
diff --git a/InvenTree/label/apps.py b/InvenTree/label/apps.py
index d7677f68b0..595c7aa62c 100644
--- a/InvenTree/label/apps.py
+++ b/InvenTree/label/apps.py
@@ -19,7 +19,7 @@ from InvenTree.ready import (
     isPluginRegistryLoaded,
 )
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 def hashFile(filename):
diff --git a/InvenTree/label/models.py b/InvenTree/label/models.py
index 89d8d0baf3..ccc5b03843 100644
--- a/InvenTree/label/models.py
+++ b/InvenTree/label/models.py
@@ -25,12 +25,12 @@ from plugin.registry import registry
 try:
     from django_weasyprint import WeasyTemplateResponseMixin
 except OSError as err:  # pragma: no cover
-    print(f"OSError: {err}")
-    print("You may require some further system packages to be installed.")
+    print(f'OSError: {err}')
+    print('You may require some further system packages to be installed.')
     sys.exit(1)
 
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 def rename_label(instance, filename):
@@ -97,7 +97,7 @@ class LabelTemplate(MetadataMixin, models.Model):
         abstract = True
 
     # Each class of label files will be stored in a separate subdirectory
-    SUBDIR = "label"
+    SUBDIR = 'label'
 
     # Object we will be printing against (will be filled out later)
     object_to_print = None
@@ -109,7 +109,7 @@ class LabelTemplate(MetadataMixin, models.Model):
 
     def __str__(self):
         """Format a string representation of a label instance"""
-        return f"{self.name} - {self.description}"
+        return f'{self.name} - {self.description}'
 
     name = models.CharField(
         blank=False, max_length=100, verbose_name=_('Name'), help_text=_('Label name')
@@ -154,7 +154,7 @@ class LabelTemplate(MetadataMixin, models.Model):
     )
 
     filename_pattern = models.CharField(
-        default="label.pdf",
+        default='label.pdf',
         verbose_name=_('Filename Pattern'),
         help_text=_('Pattern for generating label filenames'),
         max_length=100,
@@ -265,7 +265,7 @@ class LabelTemplate(MetadataMixin, models.Model):
         wp = WeasyprintLabelMixin(
             request,
             self.template_name,
-            base_url=request.build_absolute_uri("/"),
+            base_url=request.build_absolute_uri('/'),
             presentational_hints=True,
             filename=self.generate_filename(request),
             **kwargs,
@@ -304,7 +304,7 @@ class StockItemLabel(LabelTemplate):
         """Return the API URL associated with the StockItemLabel model"""
         return reverse('api-stockitem-label-list')  # pragma: no cover
 
-    SUBDIR = "stockitem"
+    SUBDIR = 'stockitem'
 
     filters = models.CharField(
         blank=True,
@@ -343,7 +343,7 @@ class StockLocationLabel(LabelTemplate):
         """Return the API URL associated with the StockLocationLabel model"""
         return reverse('api-stocklocation-label-list')  # pragma: no cover
 
-    SUBDIR = "stocklocation"
+    SUBDIR = 'stocklocation'
 
     filters = models.CharField(
         blank=True,
diff --git a/InvenTree/label/tests.py b/InvenTree/label/tests.py
index 9f623a873b..3b64a7e68d 100644
--- a/InvenTree/label/tests.py
+++ b/InvenTree/label/tests.py
@@ -55,13 +55,13 @@ class LabelTest(InvenTreeAPITestCase):
 
     def test_filters(self):
         """Test the label filters."""
-        filter_string = "part__pk=10"
+        filter_string = 'part__pk=10'
 
         filters = validateFilterString(filter_string, model=StockItem)
 
         self.assertEqual(type(filters), dict)
 
-        bad_filter_string = "part_pk=10"
+        bad_filter_string = 'part_pk=10'
 
         with self.assertRaises(ValidationError):
             validateFilterString(bad_filter_string, model=StockItem)
@@ -107,7 +107,7 @@ class LabelTest(InvenTreeAPITestCase):
         buffer = io.StringIO()
         buffer.write(label_data)
 
-        template = ContentFile(buffer.getvalue(), "label.html")
+        template = ContentFile(buffer.getvalue(), 'label.html')
 
         # Construct a label template
         label = PartLabel.objects.create(
@@ -140,7 +140,7 @@ class LabelTest(InvenTreeAPITestCase):
             content = f.read()
 
         # Test that each element has been rendered correctly
-        self.assertIn(f"part: {part_pk} - {part_name}", content)
+        self.assertIn(f'part: {part_pk} - {part_name}', content)
         self.assertIn(f'data: {{"part": {part_pk}}}', content)
         self.assertIn(f'http://testserver/part/{part_pk}/', content)
 
diff --git a/InvenTree/manage.py b/InvenTree/manage.py
index d2d21b0b23..9770d6ea35 100755
--- a/InvenTree/manage.py
+++ b/InvenTree/manage.py
@@ -7,17 +7,17 @@ import sys
 
 def main():
     """Run administrative tasks."""
-    os.environ.setdefault("DJANGO_SETTINGS_MODULE", "InvenTree.settings")
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'InvenTree.settings')
     try:
         from django.core.management import execute_from_command_line
     except ImportError as exc:  # pragma: no cover
         raise ImportError(
             "Couldn't import Django. Are you sure it's installed and "
-            "available on your PYTHONPATH environment variable? Did you "
-            "forget to activate a virtual environment?"
+            'available on your PYTHONPATH environment variable? Did you '
+            'forget to activate a virtual environment?'
         ) from exc
     execute_from_command_line(sys.argv)
 
 
-if __name__ == "__main__":
+if __name__ == '__main__':
     main()
diff --git a/InvenTree/order/api.py b/InvenTree/order/api.py
index e503efcd74..622cb32cae 100644
--- a/InvenTree/order/api.py
+++ b/InvenTree/order/api.py
@@ -86,7 +86,7 @@ class OrderFilter(rest_filters.FilterSet):
     """Base class for custom API filters for the OrderList endpoint."""
 
     # Filter against order status
-    status = rest_filters.NumberFilter(label="Order Status", method='filter_status')
+    status = rest_filters.NumberFilter(label='Order Status', method='filter_status')
 
     def filter_status(self, queryset, name, value):
         """Filter by integer status code"""
@@ -94,7 +94,7 @@ class OrderFilter(rest_filters.FilterSet):
 
     # Exact match for reference
     reference = rest_filters.CharFilter(
-        label='Filter by exact reference', field_name='reference', lookup_expr="iexact"
+        label='Filter by exact reference', field_name='reference', lookup_expr='iexact'
     )
 
     assigned_to_me = rest_filters.BooleanFilter(
@@ -155,7 +155,7 @@ class LineItemFilter(rest_filters.FilterSet):
     )
 
     has_pricing = rest_filters.BooleanFilter(
-        label="Has Pricing", method='filter_has_pricing'
+        label='Has Pricing', method='filter_has_pricing'
     )
 
     def filter_has_pricing(self, queryset, name, value):
@@ -271,7 +271,7 @@ class PurchaseOrderList(PurchaseOrderMixin, APIDownloadMixin, ListCreateAPI):
 
         filedata = dataset.export(export_format)
 
-        filename = f"InvenTree_PurchaseOrders.{export_format}"
+        filename = f'InvenTree_PurchaseOrders.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -518,7 +518,7 @@ class PurchaseOrderLineItemList(
 
         filedata = dataset.export(export_format)
 
-        filename = f"InvenTree_PurchaseOrderItems.{export_format}"
+        filename = f'InvenTree_PurchaseOrderItems.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -567,7 +567,7 @@ class PurchaseOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
         """Download this queryset as a file"""
         dataset = PurchaseOrderExtraLineResource().export(queryset=queryset)
         filedata = dataset.export(export_format)
-        filename = f"InvenTree_ExtraPurchaseOrderLines.{export_format}"
+        filename = f'InvenTree_ExtraPurchaseOrderLines.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -665,7 +665,7 @@ class SalesOrderList(SalesOrderMixin, APIDownloadMixin, ListCreateAPI):
 
         filedata = dataset.export(export_format)
 
-        filename = f"InvenTree_SalesOrders.{export_format}"
+        filename = f'InvenTree_SalesOrders.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -809,7 +809,7 @@ class SalesOrderLineItemList(SalesOrderLineItemMixin, APIDownloadMixin, ListCrea
         dataset = SalesOrderLineItemResource().export(queryset=queryset)
         filedata = dataset.export(export_format)
 
-        filename = f"InvenTree_SalesOrderItems.{export_format}"
+        filename = f'InvenTree_SalesOrderItems.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -836,7 +836,7 @@ class SalesOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
         """Download this queryset as a file"""
         dataset = SalesOrderExtraLineResource().export(queryset=queryset)
         filedata = dataset.export(export_format)
-        filename = f"InvenTree_ExtraSalesOrderLines.{export_format}"
+        filename = f'InvenTree_ExtraSalesOrderLines.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -1127,7 +1127,7 @@ class ReturnOrderList(ReturnOrderMixin, APIDownloadMixin, ListCreateAPI):
         """Download this queryset as a file"""
         dataset = ReturnOrderResource().export(queryset=queryset)
         filedata = dataset.export(export_format)
-        filename = f"InvenTree_ReturnOrders.{export_format}"
+        filename = f'InvenTree_ReturnOrders.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -1274,7 +1274,7 @@ class ReturnOrderLineItemList(
     def download_queryset(self, queryset, export_format):
         """Download the requested queryset as a file"""
         raise NotImplementedError(
-            "download_queryset not yet implemented for this endpoint"
+            'download_queryset not yet implemented for this endpoint'
         )
 
     filter_backends = SEARCH_ORDER_FILTER
@@ -1303,7 +1303,7 @@ class ReturnOrderExtraLineList(GeneralExtraLineList, ListCreateAPI):
 
     def download_queryset(self, queryset, export_format):
         """Download this queryset as a file"""
-        raise NotImplementedError("download_queryset not yet implemented")
+        raise NotImplementedError('download_queryset not yet implemented')
 
 
 class ReturnOrderExtraLineDetail(RetrieveUpdateDestroyAPI):
@@ -1339,9 +1339,9 @@ class OrderCalendarExport(ICalFeed):
 
     instance_url = get_base_url()
 
-    instance_url = instance_url.replace("http://", "").replace("https://", "")
+    instance_url = instance_url.replace('http://', '').replace('https://', '')
     timezone = settings.TIME_ZONE
-    file_name = "calendar.ics"
+    file_name = 'calendar.ics'
 
     def __call__(self, request, *args, **kwargs):
         """Overload call in order to check for authentication.
@@ -1367,8 +1367,8 @@ class OrderCalendarExport(ICalFeed):
             if len(auth) == 2:
                 # NOTE: We are only support basic authentication for now.
                 #
-                if auth[0].lower() == "basic":
-                    uname, passwd = base64.b64decode(auth[1]).decode("ascii").split(':')
+                if auth[0].lower() == 'basic':
+                    uname, passwd = base64.b64decode(auth[1]).decode('ascii').split(':')
                     user = authenticate(username=uname, password=passwd)
                     if user is not None:
                         if user.is_active:
@@ -1383,7 +1383,7 @@ class OrderCalendarExport(ICalFeed):
         # Still nothing - return Unauth. header with info on how to authenticate
         # Information is needed by client, eg Thunderbird
         response = JsonResponse({
-            "detail": "Authentication credentials were not provided."
+            'detail': 'Authentication credentials were not provided.'
         })
         response['WWW-Authenticate'] = 'Basic realm="api"'
         response.status_code = 401
@@ -1402,11 +1402,11 @@ class OrderCalendarExport(ICalFeed):
 
     def title(self, obj):
         """Return calendar title."""
-        if obj["ordertype"] == 'purchase-order':
+        if obj['ordertype'] == 'purchase-order':
             ordertype_title = _('Purchase Order')
-        elif obj["ordertype"] == 'sales-order':
+        elif obj['ordertype'] == 'sales-order':
             ordertype_title = _('Sales Order')
-        elif obj["ordertype"] == 'return-order':
+        elif obj['ordertype'] == 'return-order':
             ordertype_title = _('Return Order')
         else:
             ordertype_title = _('Unknown')
@@ -1433,7 +1433,7 @@ class OrderCalendarExport(ICalFeed):
                 ).filter(status__lt=PurchaseOrderStatus.COMPLETE.value)
             else:
                 outlist = models.PurchaseOrder.objects.filter(target_date__isnull=False)
-        elif obj["ordertype"] == 'sales-order':
+        elif obj['ordertype'] == 'sales-order':
             if obj['include_completed'] is False:
                 # Do not include completed (=shipped) orders from list in this case
                 # Shipped status = 20
@@ -1442,7 +1442,7 @@ class OrderCalendarExport(ICalFeed):
                 ).filter(status__lt=SalesOrderStatus.SHIPPED.value)
             else:
                 outlist = models.SalesOrder.objects.filter(target_date__isnull=False)
-        elif obj["ordertype"] == 'return-order':
+        elif obj['ordertype'] == 'return-order':
             if obj['include_completed'] is False:
                 # Do not include completed orders from list in this case
                 # Complete status = 30
@@ -1458,11 +1458,11 @@ class OrderCalendarExport(ICalFeed):
 
     def item_title(self, item):
         """Set the event title to the order reference"""
-        return f"{item.reference}"
+        return f'{item.reference}'
 
     def item_description(self, item):
         """Set the event description"""
-        return f"Company: {item.company.name}\nStatus: {item.get_status_display()}\nDescription: {item.description}"
+        return f'Company: {item.company.name}\nStatus: {item.get_status_display()}\nDescription: {item.description}'
 
     def item_start_datetime(self, item):
         """Set event start to target date. Goal is all-day event."""
diff --git a/InvenTree/order/models.py b/InvenTree/order/models.py
index 940e6ec08e..e139975f73 100644
--- a/InvenTree/order/models.py
+++ b/InvenTree/order/models.py
@@ -224,7 +224,7 @@ class Order(
         if self.company and self.contact:
             if self.contact.company != self.company:
                 raise ValidationError({
-                    "contact": _("Contact does not match selected company")
+                    'contact': _('Contact does not match selected company')
                 })
 
     @classmethod
@@ -327,7 +327,7 @@ class Order(
     @classmethod
     def get_status_class(cls):
         """Return the enumeration class which represents the 'status' field for this model"""
-        raise NotImplementedError(f"get_status_class() not implemented for {__class__}")
+        raise NotImplementedError(f'get_status_class() not implemented for {__class__}')
 
 
 class PurchaseOrder(TotalPriceMixin, Order):
@@ -454,7 +454,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
         max_length=64,
         blank=True,
         verbose_name=_('Supplier Reference'),
-        help_text=_("Supplier order reference code"),
+        help_text=_('Supplier order reference code'),
     )
 
     received_by = models.ForeignKey(
@@ -514,14 +514,14 @@ class PurchaseOrder(TotalPriceMixin, Order):
             quantity = int(quantity)
             if quantity <= 0:
                 raise ValidationError({
-                    'quantity': _("Quantity must be greater than zero")
+                    'quantity': _('Quantity must be greater than zero')
                 })
         except ValueError:
-            raise ValidationError({'quantity': _("Invalid quantity provided")})
+            raise ValidationError({'quantity': _('Invalid quantity provided')})
 
         if supplier_part.supplier != self.supplier:
             raise ValidationError({
-                'supplier': _("Part supplier must match PO supplier")
+                'supplier': _('Part supplier must match PO supplier')
             })
 
         if group:
@@ -715,11 +715,11 @@ class PurchaseOrder(TotalPriceMixin, Order):
         try:
             if quantity < 0:
                 raise ValidationError({
-                    "quantity": _("Quantity must be a positive number")
+                    'quantity': _('Quantity must be a positive number')
                 })
             quantity = InvenTree.helpers.clean_decimal(quantity)
         except TypeError:
-            raise ValidationError({"quantity": _("Invalid quantity provided")})
+            raise ValidationError({'quantity': _('Invalid quantity provided')})
 
         # Create a new stock item
         if line.part and quantity > 0:
@@ -882,7 +882,7 @@ class SalesOrder(TotalPriceMixin, Order):
         limit_choices_to={'is_customer': True},
         related_name='return_orders',
         verbose_name=_('Customer'),
-        help_text=_("Company to which the items are being sold"),
+        help_text=_('Company to which the items are being sold'),
     )
 
     @property
@@ -906,7 +906,7 @@ class SalesOrder(TotalPriceMixin, Order):
         max_length=64,
         blank=True,
         verbose_name=_('Customer Reference '),
-        help_text=_("Customer order reference code"),
+        help_text=_('Customer order reference code'),
     )
 
     shipment_date = models.DateField(
@@ -979,12 +979,12 @@ class SalesOrder(TotalPriceMixin, Order):
 
             elif self.pending_shipment_count > 0:
                 raise ValidationError(
-                    _("Order cannot be completed as there are incomplete shipments")
+                    _('Order cannot be completed as there are incomplete shipments')
                 )
 
             elif not allow_incomplete_lines and self.pending_line_count > 0:
                 raise ValidationError(
-                    _("Order cannot be completed as there are incomplete line items")
+                    _('Order cannot be completed as there are incomplete line items')
                 )
 
         except ValidationError as e:
@@ -1174,10 +1174,10 @@ class PurchaseOrderAttachment(InvenTreeAttachment):
 
     def getSubdir(self):
         """Return the directory path where PurchaseOrderAttachment files are located"""
-        return os.path.join("po_files", str(self.order.id))
+        return os.path.join('po_files', str(self.order.id))
 
     order = models.ForeignKey(
-        PurchaseOrder, on_delete=models.CASCADE, related_name="attachments"
+        PurchaseOrder, on_delete=models.CASCADE, related_name='attachments'
     )
 
 
@@ -1191,7 +1191,7 @@ class SalesOrderAttachment(InvenTreeAttachment):
 
     def getSubdir(self):
         """Return the directory path where SalesOrderAttachment files are located"""
-        return os.path.join("so_files", str(self.order.id))
+        return os.path.join('so_files', str(self.order.id))
 
     order = models.ForeignKey(
         SalesOrder, on_delete=models.CASCADE, related_name='attachments'
@@ -1342,7 +1342,7 @@ class PurchaseOrderLineItem(OrderLineItem):
 
     def __str__(self):
         """Render a string representation of a PurchaseOrderLineItem instance"""
-        return "{n} x {part} from {supplier} (for {po})".format(
+        return '{n} x {part} from {supplier} (for {po})'.format(
             n=decimal2string(self.quantity),
             part=self.part.SKU if self.part else 'unknown part',
             supplier=self.order.supplier.name if self.order.supplier else _('deleted'),
@@ -1373,7 +1373,7 @@ class PurchaseOrderLineItem(OrderLineItem):
         null=True,
         related_name='purchase_order_line_items',
         verbose_name=_('Part'),
-        help_text=_("Supplier part"),
+        help_text=_('Supplier part'),
     )
 
     received = models.DecimalField(
@@ -1483,12 +1483,12 @@ class SalesOrderLineItem(OrderLineItem):
         if self.part:
             if self.part.virtual:
                 raise ValidationError({
-                    'part': _("Virtual part cannot be assigned to a sales order")
+                    'part': _('Virtual part cannot be assigned to a sales order')
                 })
 
             if not self.part.salable:
                 raise ValidationError({
-                    'part': _("Only salable parts can be assigned to a sales order")
+                    'part': _('Only salable parts can be assigned to a sales order')
                 })
 
     order = models.ForeignKey(
@@ -1668,10 +1668,10 @@ class SalesOrderShipment(InvenTreeNotesMixin, MetadataMixin, models.Model):
         try:
             if self.shipment_date:
                 # Shipment has already been sent!
-                raise ValidationError(_("Shipment has already been sent"))
+                raise ValidationError(_('Shipment has already been sent'))
 
             if self.allocations.count() == 0:
-                raise ValidationError(_("Shipment has no allocated stock items"))
+                raise ValidationError(_('Shipment has no allocated stock items'))
 
         except ValidationError as e:
             if raise_error:
@@ -1807,7 +1807,7 @@ class SalesOrderAllocation(models.Model):
         # Ensure that we do not 'over allocate' a stock item
         build_allocation_count = self.item.build_allocation_count()
         sales_allocation_count = self.item.sales_order_allocation_count(
-            exclude_allocations={"pk": self.pk}
+            exclude_allocations={'pk': self.pk}
         )
 
         total_allocation = (
@@ -1954,7 +1954,7 @@ class ReturnOrder(TotalPriceMixin, Order):
         limit_choices_to={'is_customer': True},
         related_name='sales_orders',
         verbose_name=_('Customer'),
-        help_text=_("Company from which items are being returned"),
+        help_text=_('Company from which items are being returned'),
     )
 
     @property
@@ -1973,7 +1973,7 @@ class ReturnOrder(TotalPriceMixin, Order):
         max_length=64,
         blank=True,
         verbose_name=_('Customer Reference '),
-        help_text=_("Customer order reference code"),
+        help_text=_('Customer order reference code'),
     )
 
     issue_date = models.DateField(
@@ -2078,7 +2078,7 @@ class ReturnOrder(TotalPriceMixin, Order):
         """
         # Prevent an item from being "received" multiple times
         if line.received_date is not None:
-            logger.warning("receive_line_item called with item already returned")
+            logger.warning('receive_line_item called with item already returned')
             return
 
         stock_item = line.item
@@ -2144,7 +2144,7 @@ class ReturnOrderLineItem(OrderLineItem):
 
         if self.item and not self.item.serialized:
             raise ValidationError({
-                'item': _("Only serialized items can be assigned to a Return Order")
+                'item': _('Only serialized items can be assigned to a Return Order')
             })
 
     order = models.ForeignKey(
diff --git a/InvenTree/order/serializers.py b/InvenTree/order/serializers.py
index 4620d31421..81630af7e6 100644
--- a/InvenTree/order/serializers.py
+++ b/InvenTree/order/serializers.py
@@ -261,7 +261,7 @@ class PurchaseOrderCancelSerializer(serializers.Serializer):
         order = self.context['order']
 
         if not order.can_cancel:
-            raise ValidationError(_("Order cannot be cancelled"))
+            raise ValidationError(_('Order cannot be cancelled'))
 
         order.cancel_order()
 
@@ -286,7 +286,7 @@ class PurchaseOrderCompleteSerializer(serializers.Serializer):
         order = self.context['order']
 
         if not value and not order.is_complete:
-            raise ValidationError(_("Order has incomplete line items"))
+            raise ValidationError(_('Order has incomplete line items'))
 
         return value
 
@@ -390,7 +390,7 @@ class PurchaseOrderLineItemSerializer(InvenTreeModelSerializer):
     def validate_quantity(self, quantity):
         """Validation for the 'quantity' field"""
         if quantity <= 0:
-            raise ValidationError(_("Quantity must be greater than zero"))
+            raise ValidationError(_('Quantity must be greater than zero'))
 
         return quantity
 
@@ -517,7 +517,7 @@ class PurchaseOrderLineItemReceiveSerializer(serializers.Serializer):
     def validate_quantity(self, quantity):
         """Validation for the 'quantity' field"""
         if quantity <= 0:
-            raise ValidationError(_("Quantity must be greater than zero"))
+            raise ValidationError(_('Quantity must be greater than zero'))
 
         return quantity
 
@@ -647,7 +647,7 @@ class PurchaseOrderReceiveSerializer(serializers.Serializer):
 
             if not item['location']:
                 raise ValidationError({
-                    'location': _("Destination location must be specified")
+                    'location': _('Destination location must be specified')
                 })
 
         # Ensure barcodes are unique
@@ -1075,7 +1075,7 @@ class SalesOrderShipmentCompleteSerializer(serializers.ModelSerializer):
         shipment = self.context.get('shipment', None)
 
         if not shipment:
-            raise ValidationError(_("No shipment details provided"))
+            raise ValidationError(_('No shipment details provided'))
 
         shipment.check_can_complete(raise_error=True)
 
@@ -1135,7 +1135,7 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
 
         # Ensure that the line item points to the correct order
         if line_item.order != order:
-            raise ValidationError(_("Line item is not associated with this order"))
+            raise ValidationError(_('Line item is not associated with this order'))
 
         return line_item
 
@@ -1154,7 +1154,7 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
     def validate_quantity(self, quantity):
         """Custom validation for the 'quantity' field"""
         if quantity <= 0:
-            raise ValidationError(_("Quantity must be positive"))
+            raise ValidationError(_('Quantity must be positive'))
 
         return quantity
 
@@ -1171,13 +1171,13 @@ class SalesOrderShipmentAllocationItemSerializer(serializers.Serializer):
 
         if stock_item.serialized and quantity != 1:
             raise ValidationError({
-                'quantity': _("Quantity must be 1 for serialized stock item")
+                'quantity': _('Quantity must be 1 for serialized stock item')
             })
 
         q = normalize(stock_item.unallocated_quantity())
 
         if quantity > q:
-            raise ValidationError({'quantity': _(f"Available quantity ({q}) exceeded")})
+            raise ValidationError({'quantity': _(f'Available quantity ({q}) exceeded')})
 
         return data
 
@@ -1197,7 +1197,7 @@ class SalesOrderCompleteSerializer(serializers.Serializer):
         order = self.context['order']
 
         if not value and not order.is_completed():
-            raise ValidationError(_("Order has incomplete line items"))
+            raise ValidationError(_('Order has incomplete line items'))
 
         return value
 
@@ -1274,7 +1274,7 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
 
         # Ensure that the line item points to the correct order
         if line_item.order != order:
-            raise ValidationError(_("Line item is not associated with this order"))
+            raise ValidationError(_('Line item is not associated with this order'))
 
         return line_item
 
@@ -1283,8 +1283,8 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
     )
 
     serial_numbers = serializers.CharField(
-        label=_("Serial Numbers"),
-        help_text=_("Enter serial numbers to allocate"),
+        label=_('Serial Numbers'),
+        help_text=_('Enter serial numbers to allocate'),
         required=True,
         allow_blank=False,
     )
@@ -1306,10 +1306,10 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
         order = self.context['order']
 
         if shipment.shipment_date is not None:
-            raise ValidationError(_("Shipment has already been shipped"))
+            raise ValidationError(_('Shipment has already been shipped'))
 
         if shipment.order != order:
-            raise ValidationError(_("Shipment is not associated with this order"))
+            raise ValidationError(_('Shipment is not associated with this order'))
 
         return shipment
 
@@ -1356,16 +1356,16 @@ class SalesOrderSerialAllocationSerializer(serializers.Serializer):
                 serials_allocated.append(str(serial))
 
         if len(serials_not_exist) > 0:
-            error_msg = _("No match found for the following serial numbers")
-            error_msg += ": "
-            error_msg += ",".join(serials_not_exist)
+            error_msg = _('No match found for the following serial numbers')
+            error_msg += ': '
+            error_msg += ','.join(serials_not_exist)
 
             raise ValidationError({'serial_numbers': error_msg})
 
         if len(serials_allocated) > 0:
-            error_msg = _("The following serial numbers are already allocated")
-            error_msg += ": "
-            error_msg += ",".join(serials_allocated)
+            error_msg = _('The following serial numbers are already allocated')
+            error_msg += ': '
+            error_msg += ','.join(serials_allocated)
 
             raise ValidationError({'serial_numbers': error_msg})
 
@@ -1412,10 +1412,10 @@ class SalesOrderShipmentAllocationSerializer(serializers.Serializer):
         order = self.context['order']
 
         if shipment.shipment_date is not None:
-            raise ValidationError(_("Shipment has already been shipped"))
+            raise ValidationError(_('Shipment has already been shipped'))
 
         if shipment.order != order:
-            raise ValidationError(_("Shipment is not associated with this order"))
+            raise ValidationError(_('Shipment is not associated with this order'))
 
         return shipment
 
@@ -1596,10 +1596,10 @@ class ReturnOrderLineItemReceiveSerializer(serializers.Serializer):
     def validate_line_item(self, item):
         """Validation for a single line item"""
         if item.order != self.context['order']:
-            raise ValidationError(_("Line item does not match return order"))
+            raise ValidationError(_('Line item does not match return order'))
 
         if item.received:
-            raise ValidationError(_("Line item has already been received"))
+            raise ValidationError(_('Line item has already been received'))
 
         return item
 
@@ -1628,7 +1628,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
         order = self.context['order']
         if order.status != ReturnOrderStatus.IN_PROGRESS:
             raise ValidationError(
-                _("Items can only be received against orders which are in progress")
+                _('Items can only be received against orders which are in progress')
             )
 
         data = super().validate(data)
@@ -1636,7 +1636,7 @@ class ReturnOrderReceiveSerializer(serializers.Serializer):
         items = data.get('items', [])
 
         if len(items) == 0:
-            raise ValidationError(_("Line items must be provided"))
+            raise ValidationError(_('Line items must be provided'))
 
         return data
 
diff --git a/InvenTree/order/tasks.py b/InvenTree/order/tasks.py
index f97bcd2ac6..da621626c7 100644
--- a/InvenTree/order/tasks.py
+++ b/InvenTree/order/tasks.py
@@ -76,7 +76,7 @@ def notify_overdue_sales_order(so: order.models.SalesOrder):
     context = {
         'order': so,
         'name': name,
-        'message': _(f"Sales order {so} is now overdue"),
+        'message': _(f'Sales order {so} is now overdue'),
         'link': InvenTree.helpers_model.construct_absolute_url(so.get_absolute_url()),
         'template': {'html': 'email/overdue_sales_order.html', 'subject': name},
     }
diff --git a/InvenTree/order/test_api.py b/InvenTree/order/test_api.py
index 7540e437ff..906a5c229a 100644
--- a/InvenTree/order/test_api.py
+++ b/InvenTree/order/test_api.py
@@ -237,7 +237,7 @@ class PurchaseOrderTest(OrderTest):
         self.assignRole('purchase_order.add')
 
         url = reverse('api-po-list')
-        huge_number = "PO-92233720368547758089999999999999999"
+        huge_number = 'PO-92233720368547758089999999999999999'
 
         response = self.post(
             url,
@@ -333,7 +333,7 @@ class PurchaseOrderTest(OrderTest):
         response = self.delete(url, expected_code=403)
 
         # Now, add the "delete" permission!
-        self.assignRole("purchase_order.delete")
+        self.assignRole('purchase_order.delete')
 
         response = self.delete(url, expected_code=204)
 
@@ -589,7 +589,7 @@ class PurchaseOrderTest(OrderTest):
 
         resp_dict = response.json()
         self.assertEqual(
-            resp_dict['detail'], "Authentication credentials were not provided."
+            resp_dict['detail'], 'Authentication credentials were not provided.'
         )
 
     def test_po_calendar_auth(self):
@@ -731,7 +731,7 @@ class PurchaseOrderReceiveTest(OrderTest):
     def test_no_items(self):
         """Test with an empty list of items."""
         data = self.post(
-            self.url, {"items": [], "location": None}, expected_code=400
+            self.url, {'items': [], 'location': None}, expected_code=400
         ).data
 
         self.assertIn('Line items must be provided', str(data))
@@ -743,14 +743,14 @@ class PurchaseOrderReceiveTest(OrderTest):
         """Test than errors are returned as expected for invalid data."""
         data = self.post(
             self.url,
-            {"items": [{"line_item": 12345, "location": 12345}]},
+            {'items': [{'line_item': 12345, 'location': 12345}]},
             expected_code=400,
         ).data
 
         items = data['items'][0]
 
         self.assertIn('Invalid pk "12345"', str(items['line_item']))
-        self.assertIn("object does not exist", str(items['location']))
+        self.assertIn('object does not exist', str(items['location']))
 
         # No new stock items have been created
         self.assertEqual(self.n, StockItem.objects.count())
@@ -760,8 +760,8 @@ class PurchaseOrderReceiveTest(OrderTest):
         data = self.post(
             self.url,
             {
-                "items": [
-                    {"line_item": 22, "location": 1, "status": 99999, "quantity": 5}
+                'items': [
+                    {'line_item': 22, 'location': 1, 'status': 99999, 'quantity': 5}
                 ]
             },
             expected_code=400,
@@ -1330,7 +1330,7 @@ class SalesOrderTest(OrderTest):
                 {'export': fmt},
                 decode=True if fmt == 'csv' else False,
                 expected_code=200,
-                expected_fn=f"InvenTree_SalesOrders.{fmt}",
+                expected_fn=f'InvenTree_SalesOrders.{fmt}',
             )
 
 
@@ -1357,7 +1357,7 @@ class SalesOrderLineItemTest(OrderTest):
                         order=so,
                         part=part,
                         quantity=(idx + 1) * 5,
-                        reference=f"Order {so.reference} - line {idx}",
+                        reference=f'Order {so.reference} - line {idx}',
                     )
                 )
 
@@ -1376,7 +1376,7 @@ class SalesOrderLineItemTest(OrderTest):
         self.assertEqual(len(response.data), n)
 
         # List *all* lines, but paginate
-        response = self.get(self.url, {"limit": 5}, expected_code=200)
+        response = self.get(self.url, {'limit': 5}, expected_code=200)
 
         self.assertEqual(response.data['count'], n)
         self.assertEqual(len(response.data['results']), 5)
@@ -1530,9 +1530,9 @@ class SalesOrderAllocateTest(OrderTest):
         data = {
             'items': [
                 {
-                    "line_item": line.pk,
-                    "stock_item": part.stock_items.last().pk,
-                    "quantity": 0,
+                    'line_item': line.pk,
+                    'stock_item': part.stock_items.last().pk,
+                    'quantity': 0,
                 }
             ]
         }
@@ -1576,16 +1576,16 @@ class SalesOrderAllocateTest(OrderTest):
         # First, check that there are no line items allocated against this SalesOrder
         self.assertEqual(self.order.stock_allocations.count(), 0)
 
-        data = {"items": [], "shipment": self.shipment.pk}
+        data = {'items': [], 'shipment': self.shipment.pk}
 
         for line in self.order.lines.all():
             stock_item = line.part.stock_items.last()
 
             # Fully-allocate each line
             data['items'].append({
-                "line_item": line.pk,
-                "stock_item": stock_item.pk,
-                "quantity": 5,
+                'line_item': line.pk,
+                'stock_item': stock_item.pk,
+                'quantity': 5,
             })
 
         self.post(self.url, data, expected_code=201)
@@ -1603,7 +1603,7 @@ class SalesOrderAllocateTest(OrderTest):
         # First, check that there are no line items allocated against this SalesOrder
         self.assertEqual(self.order.stock_allocations.count(), 0)
 
-        data = {"items": [], "shipment": self.shipment.pk}
+        data = {'items': [], 'shipment': self.shipment.pk}
 
         def check_template(line_item):
             return line_item.part.is_template
@@ -1619,9 +1619,9 @@ class SalesOrderAllocateTest(OrderTest):
 
             # Fully-allocate each line
             data['items'].append({
-                "line_item": line.pk,
-                "stock_item": stock_item.pk,
-                "quantity": 5,
+                'line_item': line.pk,
+                'stock_item': stock_item.pk,
+                'quantity': 5,
             })
 
         self.post(self.url, data, expected_code=201)
@@ -1719,8 +1719,8 @@ class SalesOrderAllocateTest(OrderTest):
                     url,
                     {
                         'order': order.pk,
-                        'reference': f"SH{idx + 1}",
-                        'tracking_number': f"TRK_{order.pk}_{idx}",
+                        'reference': f'SH{idx + 1}',
+                        'tracking_number': f'TRK_{order.pk}_{idx}',
                     },
                     expected_code=201,
                 )
@@ -1932,7 +1932,7 @@ class ReturnOrderTests(InvenTreeAPITestCase):
         # Issue the order (via the API)
         self.assertIsNone(rma.issue_date)
         self.post(
-            reverse("api-return-order-issue", kwargs={"pk": rma.pk}), expected_code=201
+            reverse('api-return-order-issue', kwargs={'pk': rma.pk}), expected_code=201
         )
 
         rma.refresh_from_db()
diff --git a/InvenTree/order/test_migrations.py b/InvenTree/order/test_migrations.py
index b55d572939..407468cd51 100644
--- a/InvenTree/order/test_migrations.py
+++ b/InvenTree/order/test_migrations.py
@@ -30,8 +30,8 @@ class TestRefIntMigrations(MigratorTestCase):
         for ii in range(10):
             order = PurchaseOrder.objects.create(
                 supplier=supplier,
-                reference=f"{ii}-abcde",
-                description="Just a test order",
+                reference=f'{ii}-abcde',
+                description='Just a test order',
             )
 
             # Initially, the 'reference_int' field is unavailable
@@ -40,8 +40,8 @@ class TestRefIntMigrations(MigratorTestCase):
 
             sales_order = SalesOrder.objects.create(
                 customer=supplier,
-                reference=f"{ii}-xyz",
-                description="A test sales order",
+                reference=f'{ii}-xyz',
+                description='A test sales order',
             )
 
             # Initially, the 'reference_int' field is unavailable
@@ -67,8 +67,8 @@ class TestRefIntMigrations(MigratorTestCase):
         SalesOrder = self.new_state.apps.get_model('order', 'salesorder')
 
         for ii in range(10):
-            po = PurchaseOrder.objects.get(reference=f"{ii}-abcde")
-            so = SalesOrder.objects.get(reference=f"{ii}-xyz")
+            po = PurchaseOrder.objects.get(reference=f'{ii}-abcde')
+            so = SalesOrder.objects.get(reference=f'{ii}-xyz')
 
             # The integer reference field must have been correctly updated
             self.assertEqual(po.reference_int, ii)
@@ -166,8 +166,8 @@ class TestAdditionalLineMigration(MigratorTestCase):
         for ii in range(10):
             order = PurchaseOrder.objects.create(
                 supplier=supplier,
-                reference=f"{ii}-abcde",
-                description="Just a test order",
+                reference=f'{ii}-abcde',
+                description='Just a test order',
             )
             order.lines.create(part=supplierpart, quantity=12, received=1)
             order.lines.create(quantity=12, received=1)
@@ -188,7 +188,7 @@ class TestAdditionalLineMigration(MigratorTestCase):
         """Test that the the PO lines where converted correctly."""
         PurchaseOrder = self.new_state.apps.get_model('order', 'purchaseorder')
         for ii in range(10):
-            po = PurchaseOrder.objects.get(reference=f"{ii}-abcde")
+            po = PurchaseOrder.objects.get(reference=f'{ii}-abcde')
             self.assertEqual(po.extra_lines.count(), 1)
             self.assertEqual(po.lines.count(), 1)
 
diff --git a/InvenTree/order/test_sales_order.py b/InvenTree/order/test_sales_order.py
index fe20b2c10c..9fda6261de 100644
--- a/InvenTree/order/test_sales_order.py
+++ b/InvenTree/order/test_sales_order.py
@@ -33,7 +33,7 @@ class SalesOrderTest(TestCase):
         """Initial setup for this set of unit tests"""
         # Create a Company to ship the goods to
         cls.customer = Company.objects.create(
-            name="ABC Co", description="My customer", is_customer=True
+            name='ABC Co', description='My customer', is_customer=True
         )
 
         # Create a Part to ship
@@ -72,7 +72,7 @@ class SalesOrderTest(TestCase):
 
         # Create an extra line
         cls.extraline = SalesOrderExtraLine.objects.create(
-            quantity=1, order=cls.order, reference="Extra line"
+            quantity=1, order=cls.order, reference='Extra line'
         )
 
     def test_so_reference(self):
diff --git a/InvenTree/order/tests.py b/InvenTree/order/tests.py
index cba7904e1f..bdfc327f86 100644
--- a/InvenTree/order/tests.py
+++ b/InvenTree/order/tests.py
@@ -46,7 +46,7 @@ class OrderTest(TestCase):
             self.assertEqual(order.reference, f'PO-{pk:04d}')
 
         line = PurchaseOrderLineItem.objects.get(pk=1)
-        self.assertEqual(str(line), "100 x ACME0001 from ACME (for PO-0001 - ACME)")
+        self.assertEqual(str(line), '100 x ACME0001 from ACME (for PO-0001 - ACME)')
 
     def test_rebuild_reference(self):
         """Test that the reference_int field is correctly updated when the model is saved"""
@@ -236,7 +236,7 @@ class OrderTest(TestCase):
 
         # Create a new PurchaseOrder
         po = PurchaseOrder.objects.create(
-            supplier=sup, reference=f"PO-{n + 1}", description='Some PO'
+            supplier=sup, reference=f'PO-{n + 1}', description='Some PO'
         )
 
         # Add line items
diff --git a/InvenTree/order/views.py b/InvenTree/order/views.py
index 49e50141e8..4c77374535 100644
--- a/InvenTree/order/views.py
+++ b/InvenTree/order/views.py
@@ -32,7 +32,7 @@ from .models import (
     SalesOrderLineItem,
 )
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 class PurchaseOrderIndex(InvenTreeRoleMixin, ListView):
@@ -115,9 +115,9 @@ class PurchaseOrderUpload(FileManagementFormView):
         'order/order_wizard/match_parts.html',
     ]
     form_steps_description = [
-        _("Upload File"),
-        _("Match Fields"),
-        _("Match Supplier Parts"),
+        _('Upload File'),
+        _('Match Fields'),
+        _('Match Supplier Parts'),
     ]
     form_field_map = {
         'item_select': 'part',
@@ -294,7 +294,7 @@ class SalesOrderExport(AjaxView):
 
         export_format = request.GET.get('format', 'csv')
 
-        filename = f"{str(order)} - {order.customer.name}.{export_format}"
+        filename = f'{str(order)} - {order.customer.name}.{export_format}'
 
         dataset = SalesOrderLineItemResource().export(queryset=order.lines.all())
 
diff --git a/InvenTree/part/api.py b/InvenTree/part/api.py
index c7f744f46d..62e856af6b 100644
--- a/InvenTree/part/api.py
+++ b/InvenTree/part/api.py
@@ -114,7 +114,7 @@ class CategoryList(CategoryMixin, APIDownloadMixin, ListCreateAPI):
         """Download the filtered queryset as a data file"""
         dataset = PartCategoryResource().export(queryset=queryset)
         filedata = dataset.export(export_format)
-        filename = f"InvenTree_Categories.{export_format}"
+        filename = f'InvenTree_Categories.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -682,23 +682,23 @@ class PartRequirements(RetrieveAPI):
         part = self.get_object()
 
         data = {
-            "available_stock": part.available_stock,
-            "on_order": part.on_order,
-            "required_build_order_quantity": part.required_build_order_quantity(),
-            "allocated_build_order_quantity": part.build_order_allocation_count(),
-            "required_sales_order_quantity": part.required_sales_order_quantity(),
-            "allocated_sales_order_quantity": part.sales_order_allocation_count(
+            'available_stock': part.available_stock,
+            'on_order': part.on_order,
+            'required_build_order_quantity': part.required_build_order_quantity(),
+            'allocated_build_order_quantity': part.build_order_allocation_count(),
+            'required_sales_order_quantity': part.required_sales_order_quantity(),
+            'allocated_sales_order_quantity': part.sales_order_allocation_count(
                 pending=True
             ),
         }
 
-        data["allocated"] = (
-            data["allocated_build_order_quantity"]
-            + data["allocated_sales_order_quantity"]
+        data['allocated'] = (
+            data['allocated_build_order_quantity']
+            + data['allocated_sales_order_quantity']
         )
-        data["required"] = (
-            data["required_build_order_quantity"]
-            + data["required_sales_order_quantity"]
+        data['required'] = (
+            data['required_build_order_quantity']
+            + data['required_sales_order_quantity']
         )
 
         return Response(data)
@@ -850,7 +850,7 @@ class PartFilter(rest_filters.FilterSet):
     IPN = rest_filters.CharFilter(
         label='Filter by exact IPN (internal part number)',
         field_name='IPN',
-        lookup_expr="iexact",
+        lookup_expr='iexact',
     )
 
     # Regex match for IPN
@@ -895,7 +895,7 @@ class PartFilter(rest_filters.FilterSet):
         return queryset.filter(Q(unallocated_stock__lte=0))
 
     convert_from = rest_filters.ModelChoiceFilter(
-        label="Can convert from",
+        label='Can convert from',
         queryset=Part.objects.all(),
         method='filter_convert_from',
     )
@@ -909,7 +909,7 @@ class PartFilter(rest_filters.FilterSet):
         return queryset
 
     exclude_tree = rest_filters.ModelChoiceFilter(
-        label="Exclude Part tree",
+        label='Exclude Part tree',
         queryset=Part.objects.all(),
         method='filter_exclude_tree',
     )
@@ -947,7 +947,7 @@ class PartFilter(rest_filters.FilterSet):
         return queryset.filter(id__in=[p.pk for p in bom_parts])
 
     has_pricing = rest_filters.BooleanFilter(
-        label="Has Pricing", method="filter_has_pricing"
+        label='Has Pricing', method='filter_has_pricing'
     )
 
     def filter_has_pricing(self, queryset, name, value):
@@ -961,7 +961,7 @@ class PartFilter(rest_filters.FilterSet):
         return queryset.filter(q_a | q_b).distinct()
 
     stocktake = rest_filters.BooleanFilter(
-        label="Has stocktake", method='filter_has_stocktake'
+        label='Has stocktake', method='filter_has_stocktake'
     )
 
     def filter_has_stocktake(self, queryset, name, value):
@@ -997,7 +997,7 @@ class PartFilter(rest_filters.FilterSet):
         return queryset.exclude(Q(in_stock=0) & ~Q(stock_item_count=0))
 
     default_location = rest_filters.ModelChoiceFilter(
-        label="Default Location", queryset=StockLocation.objects.all()
+        label='Default Location', queryset=StockLocation.objects.all()
     )
 
     is_template = rest_filters.BooleanFilter()
@@ -1095,7 +1095,7 @@ class PartList(PartMixin, APIDownloadMixin, ListCreateAPI):
         dataset = PartResource().export(queryset=queryset)
 
         filedata = dataset.export(export_format)
-        filename = f"InvenTree_Parts.{export_format}"
+        filename = f'InvenTree_Parts.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -1668,7 +1668,7 @@ class BomFilter(rest_filters.FilterSet):
     )
 
     available_stock = rest_filters.BooleanFilter(
-        label="Has available stock", method="filter_available_stock"
+        label='Has available stock', method='filter_available_stock'
     )
 
     def filter_available_stock(self, queryset, name, value):
@@ -1677,7 +1677,7 @@ class BomFilter(rest_filters.FilterSet):
             return queryset.filter(available_stock__gt=0)
         return queryset.filter(available_stock=0)
 
-    on_order = rest_filters.BooleanFilter(label="On order", method="filter_on_order")
+    on_order = rest_filters.BooleanFilter(label='On order', method='filter_on_order')
 
     def filter_on_order(self, queryset, name, value):
         """Filter the queryset based on whether each line item has any stock on order"""
@@ -1686,7 +1686,7 @@ class BomFilter(rest_filters.FilterSet):
         return queryset.filter(on_order=0)
 
     has_pricing = rest_filters.BooleanFilter(
-        label="Has Pricing", method="filter_has_pricing"
+        label='Has Pricing', method='filter_has_pricing'
     )
 
     def filter_has_pricing(self, queryset, name, value):
diff --git a/InvenTree/part/apps.py b/InvenTree/part/apps.py
index 550c18250a..b7f45bafd4 100644
--- a/InvenTree/part/apps.py
+++ b/InvenTree/part/apps.py
@@ -12,7 +12,7 @@ from InvenTree.ready import (
     isPluginRegistryLoaded,
 )
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 class PartConfig(AppConfig):
@@ -67,11 +67,11 @@ class PartConfig(AppConfig):
             if items.count() > 0:
                 # Find any pricing objects which have the 'scheduled_for_update' flag set
                 logger.info(
-                    "Resetting update flags for %s pricing objects...", items.count()
+                    'Resetting update flags for %s pricing objects...', items.count()
                 )
 
                 for pricing in items:
                     pricing.scheduled_for_update = False
                     pricing.save()
         except Exception:
-            logger.exception("Failed to reset pricing flags - database not ready")
+            logger.exception('Failed to reset pricing flags - database not ready')
diff --git a/InvenTree/part/bom.py b/InvenTree/part/bom.py
index bbaeea4d42..88b67d17e0 100644
--- a/InvenTree/part/bom.py
+++ b/InvenTree/part/bom.py
@@ -269,14 +269,14 @@ def ExportBom(
 
                             # Generate column names for this supplier
                             k_sup = (
-                                str(_("Supplier"))
-                                + "_"
+                                str(_('Supplier'))
+                                + '_'
                                 + str(mp_idx)
-                                + "_"
+                                + '_'
                                 + str(sp_idx)
                             )
                             k_sku = (
-                                str(_("SKU")) + "_" + str(mp_idx) + "_" + str(sp_idx)
+                                str(_('SKU')) + '_' + str(mp_idx) + '_' + str(sp_idx)
                             )
 
                             try:
@@ -307,8 +307,8 @@ def ExportBom(
                     supplier_sku = sp_part.SKU
 
                     # Generate column names for this supplier
-                    k_sup = str(_("Supplier")) + "_" + str(sp_idx)
-                    k_sku = str(_("SKU")) + "_" + str(sp_idx)
+                    k_sup = str(_('Supplier')) + '_' + str(sp_idx)
+                    k_sku = str(_('SKU')) + '_' + str(sp_idx)
 
                     try:
                         manufacturer_cols[k_sup].update({bom_idx: supplier_name})
@@ -322,6 +322,6 @@ def ExportBom(
 
     data = dataset.export(fmt)
 
-    filename = f"{part.full_name}_BOM.{fmt}"
+    filename = f'{part.full_name}_BOM.{fmt}'
 
     return DownloadFile(data, filename)
diff --git a/InvenTree/part/helpers.py b/InvenTree/part/helpers.py
index c95eaa83e4..e8960b29a7 100644
--- a/InvenTree/part/helpers.py
+++ b/InvenTree/part/helpers.py
@@ -69,7 +69,7 @@ def render_part_full_name(part) -> str:
             return template.render(part=part)
         except Exception as e:
             logger.warning(
-                "exception while trying to create full name for part %s: %s",
+                'exception while trying to create full name for part %s: %s',
                 part.name,
                 e,
             )
@@ -80,7 +80,7 @@ def render_part_full_name(part) -> str:
 
 
 # Subdirectory for storing part images
-PART_IMAGE_DIR = "part_images"
+PART_IMAGE_DIR = 'part_images'
 
 
 def get_part_image_directory() -> str:
diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py
index cb10243719..5dfe91e3e0 100644
--- a/InvenTree/part/models.py
+++ b/InvenTree/part/models.py
@@ -67,7 +67,7 @@ from InvenTree.status_codes import (
 from order import models as OrderModels
 from stock import models as StockModels
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 class PartCategory(MetadataMixin, InvenTreeTree):
@@ -85,8 +85,8 @@ class PartCategory(MetadataMixin, InvenTreeTree):
     class Meta:
         """Metaclass defines extra model properties"""
 
-        verbose_name = _("Part Category")
-        verbose_name_plural = _("Part Categories")
+        verbose_name = _('Part Category')
+        verbose_name_plural = _('Part Categories')
 
     def delete(self, *args, **kwargs):
         """Custom model deletion routine, which updates any child categories or parts.
@@ -101,7 +101,7 @@ class PartCategory(MetadataMixin, InvenTreeTree):
 
     default_location = TreeForeignKey(
         'stock.StockLocation',
-        related_name="default_categories",
+        related_name='default_categories',
         null=True,
         blank=True,
         on_delete=models.SET_NULL,
@@ -129,8 +129,8 @@ class PartCategory(MetadataMixin, InvenTreeTree):
     icon = models.CharField(
         blank=True,
         max_length=100,
-        verbose_name=_("Icon"),
-        help_text=_("Icon (optional)"),
+        verbose_name=_('Icon'),
+        help_text=_('Icon (optional)'),
     )
 
     @staticmethod
@@ -150,8 +150,8 @@ class PartCategory(MetadataMixin, InvenTreeTree):
         if self.pk and self.structural and self.partcount(False, False) > 0:
             raise ValidationError(
                 _(
-                    "You cannot make this part category structural because some parts "
-                    "are already assigned to it!"
+                    'You cannot make this part category structural because some parts '
+                    'are already assigned to it!'
                 )
             )
         super().clean()
@@ -387,8 +387,8 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
     class Meta:
         """Metaclass defines extra model properties"""
 
-        verbose_name = _("Part")
-        verbose_name_plural = _("Parts")
+        verbose_name = _('Part')
+        verbose_name_plural = _('Parts')
         ordering = ['name']
         constraints = [
             UniqueConstraint(fields=['name', 'IPN', 'revision'], name='unique_part')
@@ -482,7 +482,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
 
     def __str__(self):
         """Return a string representation of the Part (for use in the admin interface)"""
-        return f"{self.full_name} - {self.description}"
+        return f'{self.full_name} - {self.description}'
 
     def get_parts_in_bom(self, **kwargs):
         """Return a list of all parts in the BOM for this part.
@@ -686,8 +686,8 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
         if stock.exists():
             if raise_error:
                 raise ValidationError(
-                    _("Stock item with this serial number already exists")
-                    + ": "
+                    _('Stock item with this serial number already exists')
+                    + ': '
                     + serial
                 )
             else:
@@ -800,7 +800,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
             .exists()
         ):
             raise ValidationError(
-                _("Part with this Name, IPN and Revision already exists.")
+                _('Part with this Name, IPN and Revision already exists.')
             )
 
     def clean(self):
@@ -815,7 +815,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
         """
         if self.category is not None and self.category.structural:
             raise ValidationError({
-                'category': _("Parts cannot be assigned to structural part categories!")
+                'category': _('Parts cannot be assigned to structural part categories!')
             })
 
         super().clean()
@@ -989,7 +989,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
 
     units = models.CharField(
         max_length=20,
-        default="",
+        default='',
         blank=True,
         null=True,
         verbose_name=_('Units'),
@@ -1024,7 +1024,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
     salable = models.BooleanField(
         default=part_settings.part_salable_default,
         verbose_name=_('Salable'),
-        help_text=_("Can this part be sold to customers?"),
+        help_text=_('Can this part be sold to customers?'),
     )
 
     active = models.BooleanField(
@@ -1823,7 +1823,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
         min_price = normalize(min_price)
         max_price = normalize(max_price)
 
-        return f"{min_price} - {max_price}"
+        return f'{min_price} - {max_price}'
 
     def get_supplier_price_range(self, quantity=1):
         """Return the supplier price range of this part:
@@ -1872,7 +1872,7 @@ class Part(InvenTreeBarcodeMixin, InvenTreeNotesMixin, MetadataMixin, MPTTModel)
 
         for item in self.get_bom_items().select_related('sub_part'):
             if item.sub_part.pk == self.pk:
-                logger.warning("WARNING: BomItem ID %s contains itself in BOM", item.pk)
+                logger.warning('WARNING: BomItem ID %s contains itself in BOM', item.pk)
                 continue
 
             q = decimal.Decimal(quantity)
@@ -2402,7 +2402,7 @@ class PartPricing(common.models.MetaMixin):
             result = convert_money(money, target_currency)
         except MissingRate:
             logger.warning(
-                "No currency conversion rate available for %s -> %s",
+                'No currency conversion rate available for %s -> %s',
                 money.currency,
                 target_currency,
             )
@@ -2432,7 +2432,7 @@ class PartPricing(common.models.MetaMixin):
             or not Part.objects.filter(pk=self.part.pk).exists()
         ):
             logger.warning(
-                "Referenced part instance does not exist - skipping pricing update."
+                'Referenced part instance does not exist - skipping pricing update.'
             )
             return
 
@@ -2458,13 +2458,13 @@ class PartPricing(common.models.MetaMixin):
 
         if self.scheduled_for_update:
             # Ignore if the pricing is already scheduled to be updated
-            logger.debug("Pricing for %s already scheduled for update - skipping", p)
+            logger.debug('Pricing for %s already scheduled for update - skipping', p)
             return
 
         if counter > 25:
             # Prevent infinite recursion / stack depth issues
             logger.debug(
-                counter, f"Skipping pricing update for {p} - maximum depth exceeded"
+                counter, f'Skipping pricing update for {p} - maximum depth exceeded'
             )
             return
 
@@ -3260,7 +3260,7 @@ class PartAttachment(InvenTreeAttachment):
 
     def getSubdir(self):
         """Returns the media subdirectory where part attachments are stored"""
-        return os.path.join("part_files", str(self.part.id))
+        return os.path.join('part_files', str(self.part.id))
 
     part = models.ForeignKey(
         Part,
@@ -3423,7 +3423,7 @@ class PartTestTemplate(MetadataMixin, models.Model):
         for test in tests:
             if test.key == key:
                 raise ValidationError({
-                    'test_name': _("Test with this name already exists for this part")
+                    'test_name': _('Test with this name already exists for this part')
                 })
 
         super().validate_unique(exclude)
@@ -3444,35 +3444,35 @@ class PartTestTemplate(MetadataMixin, models.Model):
     test_name = models.CharField(
         blank=False,
         max_length=100,
-        verbose_name=_("Test Name"),
-        help_text=_("Enter a name for the test"),
+        verbose_name=_('Test Name'),
+        help_text=_('Enter a name for the test'),
     )
 
     description = models.CharField(
         blank=False,
         null=True,
         max_length=100,
-        verbose_name=_("Test Description"),
-        help_text=_("Enter description for this test"),
+        verbose_name=_('Test Description'),
+        help_text=_('Enter description for this test'),
     )
 
     required = models.BooleanField(
         default=True,
-        verbose_name=_("Required"),
-        help_text=_("Is this test required to pass?"),
+        verbose_name=_('Required'),
+        help_text=_('Is this test required to pass?'),
     )
 
     requires_value = models.BooleanField(
         default=False,
-        verbose_name=_("Requires Value"),
-        help_text=_("Does this test require a value when adding a test result?"),
+        verbose_name=_('Requires Value'),
+        help_text=_('Does this test require a value when adding a test result?'),
     )
 
     requires_attachment = models.BooleanField(
         default=False,
-        verbose_name=_("Requires Attachment"),
+        verbose_name=_('Requires Attachment'),
         help_text=_(
-            "Does this test require a file attachment when adding a test result?"
+            'Does this test require a file attachment when adding a test result?'
         ),
     )
 
@@ -3503,7 +3503,7 @@ class PartParameterTemplate(MetadataMixin, models.Model):
         """Return a string representation of a PartParameterTemplate instance"""
         s = str(self.name)
         if self.units:
-            s += f" ({self.units})"
+            s += f' ({self.units})'
         return s
 
     def clean(self):
@@ -3557,8 +3557,8 @@ class PartParameterTemplate(MetadataMixin, models.Model):
             ).exclude(pk=self.pk)
 
             if others.exists():
-                msg = _("Parameter template name must be unique")
-                raise ValidationError({"name": msg})
+                msg = _('Parameter template name must be unique')
+                raise ValidationError({'name': msg})
         except PartParameterTemplate.DoesNotExist:
             pass
 
@@ -3644,7 +3644,7 @@ class PartParameter(MetadataMixin, models.Model):
 
     def __str__(self):
         """String representation of a PartParameter (used in the admin interface)"""
-        return f"{self.part.full_name} : {self.template.name} = {self.data} ({self.template.units})"
+        return f'{self.part.full_name} : {self.template.name} = {self.data} ({self.template.units})'
 
     def save(self, *args, **kwargs):
         """Custom save method for the PartParameter model."""
@@ -3856,11 +3856,11 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
     class Meta:
         """Metaclass providing extra model definition"""
 
-        verbose_name = _("BOM Item")
+        verbose_name = _('BOM Item')
 
     def __str__(self):
         """Return a string representation of this BomItem instance"""
-        return f"{decimal2string(self.quantity)} x {self.sub_part.full_name} to make {self.part.full_name}"
+        return f'{decimal2string(self.quantity)} x {self.sub_part.full_name} to make {self.part.full_name}'
 
     @staticmethod
     def get_api_url():
@@ -3968,13 +3968,13 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
     optional = models.BooleanField(
         default=False,
         verbose_name=_('Optional'),
-        help_text=_("This BOM item is optional"),
+        help_text=_('This BOM item is optional'),
     )
 
     consumable = models.BooleanField(
         default=False,
         verbose_name=_('Consumable'),
-        help_text=_("This BOM item is consumable (it is not tracked in build orders)"),
+        help_text=_('This BOM item is consumable (it is not tracked in build orders)'),
     )
 
     overage = models.CharField(
@@ -4106,8 +4106,8 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
                 if self.sub_part.trackable:
                     if self.quantity != int(self.quantity):
                         raise ValidationError({
-                            "quantity": _(
-                                "Quantity must be integer value for trackable parts"
+                            'quantity': _(
+                                'Quantity must be integer value for trackable parts'
                             )
                         })
 
@@ -4204,7 +4204,7 @@ class BomItem(DataImportMixin, MetadataMixin, models.Model):
         pmin = decimal2money(pmin)
         pmax = decimal2money(pmax)
 
-        return f"{pmin} to {pmax}"
+        return f'{pmin} to {pmax}'
 
 
 @receiver(post_save, sender=BomItem, dispatch_uid='update_bom_build_lines')
@@ -4259,7 +4259,7 @@ class BomItemSubstitute(MetadataMixin, models.Model):
     class Meta:
         """Metaclass providing extra model definition"""
 
-        verbose_name = _("BOM Item Substitute")
+        verbose_name = _('BOM Item Substitute')
 
         # Prevent duplication of substitute parts
         unique_together = ('part', 'bom_item')
@@ -4280,7 +4280,7 @@ class BomItemSubstitute(MetadataMixin, models.Model):
 
         if self.part == self.bom_item.sub_part:
             raise ValidationError({
-                "part": _("Substitute part cannot be the same as the master part")
+                'part': _('Substitute part cannot be the same as the master part')
             })
 
     @staticmethod
@@ -4345,9 +4345,9 @@ class PartRelated(MetadataMixin, models.Model):
 
         if self.part_1 == self.part_2:
             raise ValidationError(
-                _("Part relationship cannot be created between a part and itself")
+                _('Part relationship cannot be created between a part and itself')
             )
 
         # Check for inverse relationship
         if PartRelated.objects.filter(part_1=self.part_2, part_2=self.part_1).exists():
-            raise ValidationError(_("Duplicate relationship already exists"))
+            raise ValidationError(_('Duplicate relationship already exists'))
diff --git a/InvenTree/part/serializers.py b/InvenTree/part/serializers.py
index 5a17740642..4e978e086f 100644
--- a/InvenTree/part/serializers.py
+++ b/InvenTree/part/serializers.py
@@ -55,7 +55,7 @@ from .models import (
     PartTestTemplate,
 )
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 class CategorySerializer(InvenTree.serializers.InvenTreeModelSerializer):
@@ -220,7 +220,7 @@ class PartThumbSerializerUpdate(InvenTree.serializers.InvenTreeModelSerializer):
         """Check that file is an image."""
         validate = imghdr.what(value)
         if not validate:
-            raise serializers.ValidationError("File is not an image")
+            raise serializers.ValidationError('File is not an image')
         return value
 
     image = InvenTree.serializers.InvenTreeAttachmentSerializerField(required=True)
@@ -346,7 +346,7 @@ class PartSetCategorySerializer(serializers.Serializer):
     def validate_parts(self, parts):
         """Validate the selected parts"""
         if len(parts) == 0:
-            raise serializers.ValidationError(_("No parts selected"))
+            raise serializers.ValidationError(_('No parts selected'))
 
         return parts
 
@@ -881,7 +881,7 @@ class PartSerializer(
                     )
                 except IntegrityError:
                     logger.exception(
-                        "Could not create new PartParameter for part %s", instance
+                        'Could not create new PartParameter for part %s', instance
                     )
 
         # Create initial stock entry
@@ -945,7 +945,7 @@ class PartSerializer(
             remote_img.save(buffer, format=fmt)
 
             # Construct a simplified name for the image
-            filename = f"part_{part.pk}_image.{fmt.lower()}"
+            filename = f'part_{part.pk}_image.{fmt.lower()}'
 
             part.image.save(filename, ContentFile(buffer.getvalue()))
 
@@ -1071,12 +1071,12 @@ class PartStocktakeReportGenerateSerializer(serializers.Serializer):
         # Stocktake functionality must be enabled
         if not common.models.InvenTreeSetting.get_setting('STOCKTAKE_ENABLE', False):
             raise serializers.ValidationError(
-                _("Stocktake functionality is not enabled")
+                _('Stocktake functionality is not enabled')
             )
 
         # Check that background worker is running
         if not InvenTree.status.is_worker_running():
-            raise serializers.ValidationError(_("Background worker check failed"))
+            raise serializers.ValidationError(_('Background worker check failed'))
 
         return data
 
@@ -1381,7 +1381,7 @@ class BomItemSerializer(InvenTree.serializers.InvenTreeModelSerializer):
     def validate_quantity(self, quantity):
         """Perform validation for the BomItem quantity field"""
         if quantity <= 0:
-            raise serializers.ValidationError(_("Quantity must be greater than zero"))
+            raise serializers.ValidationError(_('Quantity must be greater than zero'))
 
         return quantity
 
@@ -1680,7 +1680,7 @@ class BomImportExtractSerializer(InvenTree.serializers.DataFileExtractSerializer
 
         if not any(col in self.columns for col in part_columns):
             # At least one part column is required!
-            raise serializers.ValidationError(_("No part column specified"))
+            raise serializers.ValidationError(_('No part column specified'))
 
     @staticmethod
     def process_row(row):
@@ -1768,7 +1768,7 @@ class BomImportSubmitSerializer(serializers.Serializer):
         items = data['items']
 
         if len(items) == 0:
-            raise serializers.ValidationError(_("At least one BOM item is required"))
+            raise serializers.ValidationError(_('At least one BOM item is required'))
 
         data = super().validate(data)
 
@@ -1798,7 +1798,7 @@ class BomImportSubmitSerializer(serializers.Serializer):
                 bom_items.append(BomItem(**item))
 
             if len(bom_items) > 0:
-                logger.info("Importing %s BOM items", len(bom_items))
+                logger.info('Importing %s BOM items', len(bom_items))
                 BomItem.objects.bulk_create(bom_items)
 
         except Exception as e:
diff --git a/InvenTree/part/stocktake.py b/InvenTree/part/stocktake.py
index 7b3d12f3b7..4d89b263ec 100644
--- a/InvenTree/part/stocktake.py
+++ b/InvenTree/part/stocktake.py
@@ -62,7 +62,7 @@ def perform_stocktake(
 
     if not pricing.is_valid:
         # If pricing is not valid, let's update
-        logger.info("Pricing not valid for %s - updating", target)
+        logger.info('Pricing not valid for %s - updating', target)
         pricing.update_pricing(cascade=False)
         pricing.refresh_from_db()
 
@@ -204,10 +204,10 @@ def generate_stocktake_report(**kwargs):
     n_parts = parts.count()
 
     if n_parts == 0:
-        logger.info("No parts selected for stocktake report - exiting")
+        logger.info('No parts selected for stocktake report - exiting')
         return
 
-    logger.info("Generating new stocktake report for %s parts", n_parts)
+    logger.info('Generating new stocktake report for %s parts', n_parts)
 
     base_currency = common.settings.currency_code_default()
 
@@ -266,7 +266,7 @@ def generate_stocktake_report(**kwargs):
     buffer.write(dataset.export('csv'))
 
     today = datetime.now().date().isoformat()
-    filename = f"InvenTree_Stocktake_{today}.csv"
+    filename = f'InvenTree_Stocktake_{today}.csv'
     report_file = ContentFile(buffer.getvalue(), name=filename)
 
     if generate_report:
@@ -295,7 +295,7 @@ def generate_stocktake_report(**kwargs):
 
     t_stocktake = time.time() - t_start
     logger.info(
-        "Generated stocktake report for %s parts in %ss",
+        'Generated stocktake report for %s parts in %ss',
         total_parts,
         round(t_stocktake, 2),
     )
diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py
index 2830b49640..604baaa907 100644
--- a/InvenTree/part/tasks.py
+++ b/InvenTree/part/tasks.py
@@ -24,7 +24,7 @@ from InvenTree.tasks import (
     scheduled_task,
 )
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 def notify_low_stock(part: part.models.Part):
@@ -33,7 +33,7 @@ def notify_low_stock(part: part.models.Part):
     - Triggered when the available stock for a given part falls be low the configured threhsold
     - A notification is delivered to any users who are 'subscribed' to this part
     """
-    name = _("Low stock notification")
+    name = _('Low stock notification')
     message = _(
         f'The available stock for {part.name} has fallen below the configured minimum level'
     )
@@ -70,7 +70,7 @@ def update_part_pricing(pricing: part.models.PartPricing, counter: int = 0):
         pricing: The target PartPricing instance to be updated
         counter: How many times this function has been called in sequence
     """
-    logger.info("Updating part pricing for %s", pricing.part)
+    logger.info('Updating part pricing for %s', pricing.part)
 
     pricing.update_pricing(counter=counter)
 
@@ -90,7 +90,7 @@ def check_missing_pricing(limit=250):
     results = part.models.PartPricing.objects.filter(updated=None)[:limit]
 
     if results.count() > 0:
-        logger.info("Found %s parts with empty pricing", results.count())
+        logger.info('Found %s parts with empty pricing', results.count())
 
         for pp in results:
             pp.schedule_for_update()
@@ -102,7 +102,7 @@ def check_missing_pricing(limit=250):
     results = part.models.PartPricing.objects.filter(updated__lte=stale_date)[:limit]
 
     if results.count() > 0:
-        logger.info("Found %s stale pricing entries", results.count())
+        logger.info('Found %s stale pricing entries', results.count())
 
         for pp in results:
             pp.schedule_for_update()
@@ -112,7 +112,7 @@ def check_missing_pricing(limit=250):
     results = part.models.PartPricing.objects.exclude(currency=currency)
 
     if results.count() > 0:
-        logger.info("Found %s pricing entries in the wrong currency", results.count())
+        logger.info('Found %s pricing entries in the wrong currency', results.count())
 
         for pp in results:
             pp.schedule_for_update()
@@ -121,7 +121,7 @@ def check_missing_pricing(limit=250):
     results = part.models.Part.objects.filter(pricing_data=None)[:limit]
 
     if results.count() > 0:
-        logger.info("Found %s parts without pricing", results.count())
+        logger.info('Found %s parts without pricing', results.count())
 
         for p in results:
             pricing = p.pricing
@@ -151,14 +151,14 @@ def scheduled_stocktake_reports():
     old_reports = part.models.PartStocktakeReport.objects.filter(date__lt=threshold)
 
     if old_reports.count() > 0:
-        logger.info("Deleting %s stale stocktake reports", old_reports.count())
+        logger.info('Deleting %s stale stocktake reports', old_reports.count())
         old_reports.delete()
 
     # Next, check if stocktake functionality is enabled
     if not common.models.InvenTreeSetting.get_setting(
         'STOCKTAKE_ENABLE', False, cache=False
     ):
-        logger.info("Stocktake functionality is not enabled - exiting")
+        logger.info('Stocktake functionality is not enabled - exiting')
         return
 
     report_n_days = int(
@@ -168,11 +168,11 @@ def scheduled_stocktake_reports():
     )
 
     if report_n_days < 1:
-        logger.info("Stocktake auto reports are disabled, exiting")
+        logger.info('Stocktake auto reports are disabled, exiting')
         return
 
     if not check_daily_holdoff('STOCKTAKE_RECENT_REPORT', report_n_days):
-        logger.info("Stocktake report was recently generated - exiting")
+        logger.info('Stocktake report was recently generated - exiting')
         return
 
     # Let's start a new stocktake report for all parts
diff --git a/InvenTree/part/templatetags/i18n.py b/InvenTree/part/templatetags/i18n.py
index e15b5e9697..ab2d440a32 100644
--- a/InvenTree/part/templatetags/i18n.py
+++ b/InvenTree/part/templatetags/i18n.py
@@ -42,15 +42,15 @@ class CustomTranslateNode(TranslateNode):
             result = result.replace(c, '')
 
         # Escape any quotes contained in the string
-        result = result.replace("'", r"\'")
+        result = result.replace("'", r'\'')
         result = result.replace('"', r'\"')
 
         # Return the 'clean' resulting string
         return result
 
 
-@register.tag("translate")
-@register.tag("trans")
+@register.tag('translate')
+@register.tag('trans')
 def do_translate(parser, token):
     """Custom translation function, lifted from https://github.com/django/django/blob/main/django/templatetags/i18n.py
 
@@ -66,7 +66,7 @@ def do_translate(parser, token):
     asvar = None
     message_context = None
     seen = set()
-    invalid_context = {"as", "noop"}
+    invalid_context = {'as', 'noop'}
 
     while remaining:
         option = remaining.pop(0)
@@ -74,9 +74,9 @@ def do_translate(parser, token):
             raise TemplateSyntaxError(
                 "The '%s' option was specified more than once." % option
             )
-        elif option == "noop":
+        elif option == 'noop':
             noop = True
-        elif option == "context":
+        elif option == 'context':
             try:
                 value = remaining.pop(0)
             except IndexError:
@@ -87,10 +87,10 @@ def do_translate(parser, token):
             if value in invalid_context:
                 raise TemplateSyntaxError(
                     "Invalid argument '%s' provided to the '%s' tag for the context "
-                    "option" % (value, bits[0])
+                    'option' % (value, bits[0])
                 )
             message_context = parser.compile_filter(value)
-        elif option == "as":
+        elif option == 'as':
             try:
                 value = remaining.pop(0)
             except IndexError:
@@ -110,26 +110,26 @@ def do_translate(parser, token):
 
 
 # Re-register tags which we have not explicitly overridden
-register.tag("blocktrans", django.templatetags.i18n.do_block_translate)
-register.tag("blocktranslate", django.templatetags.i18n.do_block_translate)
+register.tag('blocktrans', django.templatetags.i18n.do_block_translate)
+register.tag('blocktranslate', django.templatetags.i18n.do_block_translate)
 
-register.tag("language", django.templatetags.i18n.language)
+register.tag('language', django.templatetags.i18n.language)
 
 register.tag(
-    "get_available_languages", django.templatetags.i18n.do_get_available_languages
+    'get_available_languages', django.templatetags.i18n.do_get_available_languages
 )
-register.tag("get_language_info", django.templatetags.i18n.do_get_language_info)
+register.tag('get_language_info', django.templatetags.i18n.do_get_language_info)
 register.tag(
-    "get_language_info_list", django.templatetags.i18n.do_get_language_info_list
+    'get_language_info_list', django.templatetags.i18n.do_get_language_info_list
 )
-register.tag("get_current_language", django.templatetags.i18n.do_get_current_language)
+register.tag('get_current_language', django.templatetags.i18n.do_get_current_language)
 register.tag(
-    "get_current_language_bidi", django.templatetags.i18n.do_get_current_language_bidi
+    'get_current_language_bidi', django.templatetags.i18n.do_get_current_language_bidi
 )
 
-register.filter("language_name", django.templatetags.i18n.language_name)
+register.filter('language_name', django.templatetags.i18n.language_name)
 register.filter(
-    "language_name_translated", django.templatetags.i18n.language_name_translated
+    'language_name_translated', django.templatetags.i18n.language_name_translated
 )
-register.filter("language_name_local", django.templatetags.i18n.language_name_local)
-register.filter("language_bidi", django.templatetags.i18n.language_bidi)
+register.filter('language_name_local', django.templatetags.i18n.language_name_local)
+register.filter('language_bidi', django.templatetags.i18n.language_bidi)
diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py
index 14cc7e7a81..198d10ccfa 100644
--- a/InvenTree/part/templatetags/inventree_extras.py
+++ b/InvenTree/part/templatetags/inventree_extras.py
@@ -65,7 +65,7 @@ def render_date(context, date_object):
         try:
             date_object = date.fromisoformat(date_object)
         except ValueError:
-            logger.warning("Tried to convert invalid date string: %s", date_object)
+            logger.warning('Tried to convert invalid date string: %s', date_object)
             return None
 
     # We may have already pre-cached the date format by calling this already!
@@ -220,7 +220,7 @@ def python_version(*args, **kwargs):
 def inventree_version(shortstring=False, *args, **kwargs):
     """Return InvenTree version string."""
     if shortstring:
-        return _(f"{version.inventreeInstanceTitle()} v{version.inventreeVersion()}")
+        return _(f'{version.inventreeInstanceTitle()} v{version.inventreeVersion()}')
     return version.inventreeVersion()
 
 
@@ -645,18 +645,18 @@ def admin_url(user, table, pk):
     from django.urls import reverse
 
     if not djangosettings.INVENTREE_ADMIN_ENABLED:
-        return ""
+        return ''
 
     if not user.is_staff:
-        return ""
+        return ''
 
     # Check the user has the correct permission
-    perm_string = f"{app}.change_{model}"
+    perm_string = f'{app}.change_{model}'
     if not user.has_perm(perm_string):
         return ''
 
     # Fallback URL
-    url = reverse(f"admin:{app}_{model}_changelist")
+    url = reverse(f'admin:{app}_{model}_changelist')
 
     if pk:
         try:
diff --git a/InvenTree/part/test_api.py b/InvenTree/part/test_api.py
index 7baa291496..1b32f5f914 100644
--- a/InvenTree/part/test_api.py
+++ b/InvenTree/part/test_api.py
@@ -178,14 +178,14 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
         # Create child categories
         for ii in range(10):
             child = PartCategory.objects.create(
-                name=f"Child cat {ii}", description="A child category", parent=cat
+                name=f'Child cat {ii}', description='A child category', parent=cat
             )
 
             # Create parts in this category
             for jj in range(10):
                 Part.objects.create(
-                    name=f"Part xyz {jj}_{ii}",
-                    description="A test part with a description",
+                    name=f'Part xyz {jj}_{ii}',
+                    description='A test part with a description',
                     category=child,
                 )
 
@@ -351,8 +351,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
             for jj in range(3):
                 parts.append(
                     Part.objects.create(
-                        name=f"Part xyz {i}_{jj}",
-                        description="Child part of the deleted category",
+                        name=f'Part xyz {i}_{jj}',
+                        description='Child part of the deleted category',
                         category=cat_to_delete,
                     )
                 )
@@ -362,8 +362,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
             # Create child categories under the category to be deleted
             for ii in range(3):
                 child = PartCategory.objects.create(
-                    name=f"Child parent_cat {i}_{ii}",
-                    description="A child category of the deleted category",
+                    name=f'Child parent_cat {i}_{ii}',
+                    description='A child category of the deleted category',
                     parent=cat_to_delete,
                 )
                 child_categories.append(child)
@@ -372,8 +372,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
                 for jj in range(3):
                     child_categories_parts.append(
                         Part.objects.create(
-                            name=f"Part xyz {i}_{jj}_{ii}",
-                            description="Child part in the child category of the deleted category",
+                            name=f'Part xyz {i}_{jj}_{ii}',
+                            description='Child part in the child category of the deleted category',
                             category=child,
                         )
                     )
@@ -438,8 +438,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
         # Make sure that we get an error if we try to create part in the structural category
         with self.assertRaises(ValidationError):
             part = Part.objects.create(
-                name="-",
-                description="Part which shall not be created",
+                name='-',
+                description='Part which shall not be created',
                 category=structural_category,
             )
 
@@ -456,8 +456,8 @@ class PartCategoryAPITest(InvenTreeAPITestCase):
 
         # Create the test part assigned to a non-structural category
         part = Part.objects.create(
-            name="-",
-            description="Part which category will be changed to structural",
+            name='-',
+            description='Part which category will be changed to structural',
             category=non_structural_category,
         )
 
@@ -752,8 +752,8 @@ class PartAPITest(PartAPITestBase):
         for color in ['Red', 'Green', 'Blue', 'Yellow', 'Pink', 'Black']:
             variants.append(
                 Part.objects.create(
-                    name=f"{color} Variant",
-                    description="Variant part with a specific color",
+                    name=f'{color} Variant',
+                    description='Variant part with a specific color',
                     variant_of=master_part,
                     category=category,
                 )
@@ -839,7 +839,7 @@ class PartAPITest(PartAPITestBase):
         # Try to post a new test with the same name (should fail)
         response = self.post(
             url,
-            data={'part': 10004, 'test_name': "   newtest", 'description': 'dafsdf'},
+            data={'part': 10004, 'test_name': '   newtest', 'description': 'dafsdf'},
         )
 
         self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
@@ -971,8 +971,8 @@ class PartAPITest(PartAPITestBase):
 
         for i in range(10):
             gcv = Part.objects.create(
-                name=f"GC Var {i}",
-                description="Green chair variant",
+                name=f'GC Var {i}',
+                description='Green chair variant',
                 variant_of=green_chair,
             )
 
@@ -1237,10 +1237,10 @@ class PartCreationTests(PartAPITestBase):
         """Test that non-standard ASCII chars are accepted."""
         url = reverse('api-part-list')
 
-        name = "Kaltgerätestecker"
-        description = "Gerät Kaltgerätestecker strange chars should get through"
+        name = 'Kaltgerätestecker'
+        description = 'Gerät Kaltgerätestecker strange chars should get through'
 
-        data = {"name": name, "description": description, "category": 2}
+        data = {'name': name, 'description': description, 'category': 2}
 
         response = self.post(url, data, expected_code=201)
 
@@ -1284,7 +1284,7 @@ class PartCreationTests(PartAPITestBase):
             PartCategoryParameterTemplate.objects.create(
                 parameter_template=PartParameterTemplate.objects.get(pk=pk),
                 category=cat,
-                default_value=f"Value {pk}",
+                default_value=f'Value {pk}',
             )
 
         self.assertEqual(cat.parameter_templates.count(), 3)
@@ -1630,8 +1630,8 @@ class PartListTests(PartAPITestBase):
         for ii in range(100):
             parts.append(
                 Part(
-                    name=f"Extra part {ii}",
-                    description="A new part which will appear via the API",
+                    name=f'Extra part {ii}',
+                    description='A new part which will appear via the API',
                     level=0,
                     tree_id=0,
                     lft=0,
@@ -1975,15 +1975,15 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
 
         # First, create some parts
         paint = PartCategory.objects.create(
-            parent=None, name="Paint", description="Paints and such"
+            parent=None, name='Paint', description='Paints and such'
         )
 
         for color in ['Red', 'Green', 'Blue', 'Orange', 'Yellow']:
             p = Part.objects.create(
                 category=paint,
                 units='litres',
-                name=f"{color} Paint",
-                description=f"Paint which is {color} in color",
+                name=f'{color} Paint',
+                description=f'Paint which is {color} in color',
             )
 
             # Create multiple supplier parts in different sizes
@@ -1991,7 +1991,7 @@ class PartAPIAggregationTest(InvenTreeAPITestCase):
                 sp = SupplierPart.objects.create(
                     part=p,
                     supplier=supplier,
-                    SKU=f"PNT-{color}-{pk_sz}L",
+                    SKU=f'PNT-{color}-{pk_sz}L',
                     pack_quantity=str(pk_sz),
                 )
 
@@ -2137,7 +2137,7 @@ class BomItemTest(InvenTreeAPITestCase):
         url = reverse('api-bom-list')
 
         # Order by increasing quantity
-        response = self.get(f"{url}?ordering=+quantity", expected_code=200)
+        response = self.get(f'{url}?ordering=+quantity', expected_code=200)
 
         self.assertEqual(len(response.data), 6)
 
@@ -2147,7 +2147,7 @@ class BomItemTest(InvenTreeAPITestCase):
         self.assertTrue(q1 < q2)
 
         # Order by decreasing quantity
-        response = self.get(f"{url}?ordering=-quantity", expected_code=200)
+        response = self.get(f'{url}?ordering=-quantity', expected_code=200)
 
         self.assertEqual(q1, response.data[-1]['quantity'])
         self.assertEqual(q2, response.data[0]['quantity'])
@@ -2247,8 +2247,8 @@ class BomItemTest(InvenTreeAPITestCase):
         for ii in range(5):
             # Create a variant part!
             variant = Part.objects.create(
-                name=f"Variant_{ii}",
-                description="A variant part, with a description",
+                name=f'Variant_{ii}',
+                description='A variant part, with a description',
                 component=True,
                 variant_of=sub_part,
             )
@@ -2295,7 +2295,7 @@ class BomItemTest(InvenTreeAPITestCase):
         bom_item = BomItem.objects.get(pk=1)
 
         # Filter stock items which can be assigned against this stock item
-        response = self.get(stock_url, {"bom_item": bom_item.pk}, expected_code=200)
+        response = self.get(stock_url, {'bom_item': bom_item.pk}, expected_code=200)
 
         n_items = len(response.data)
 
@@ -2304,8 +2304,8 @@ class BomItemTest(InvenTreeAPITestCase):
         # Let's make some!
         for ii in range(5):
             sub_part = Part.objects.create(
-                name=f"Substitute {ii}",
-                description="A substitute part",
+                name=f'Substitute {ii}',
+                description='A substitute part',
                 component=True,
                 is_template=False,
                 assembly=False,
@@ -2322,7 +2322,7 @@ class BomItemTest(InvenTreeAPITestCase):
             self.assertEqual(len(response.data), 1)
 
             # We should also have more stock available to allocate against this BOM item!
-            response = self.get(stock_url, {"bom_item": bom_item.pk}, expected_code=200)
+            response = self.get(stock_url, {'bom_item': bom_item.pk}, expected_code=200)
 
             self.assertEqual(len(response.data), n_items + ii + 1)
 
@@ -2355,8 +2355,8 @@ class BomItemTest(InvenTreeAPITestCase):
 
         for i in range(5):
             assy = Part.objects.create(
-                name=f"Assy_{i}",
-                description="An assembly made of other parts",
+                name=f'Assy_{i}',
+                description='An assembly made of other parts',
                 active=True,
                 assembly=True,
             )
@@ -2368,8 +2368,8 @@ class BomItemTest(InvenTreeAPITestCase):
         # Create some sub-components
         for i in range(5):
             cmp = Part.objects.create(
-                name=f"Component_{i}",
-                description="A sub component",
+                name=f'Component_{i}',
+                description='A sub component',
                 active=True,
                 component=True,
             )
@@ -2403,8 +2403,8 @@ class BomItemTest(InvenTreeAPITestCase):
         for i in range(10):
             # Create a variant part
             vp = Part.objects.create(
-                name=f"Var {i}",
-                description="Variant part description field",
+                name=f'Var {i}',
+                description='Variant part description field',
                 variant_of=bom_item.sub_part,
             )
 
@@ -2523,7 +2523,7 @@ class PartInternalPriceBreakTest(InvenTreeAPITestCase):
         p.active = False
         p.save()
 
-        response = self.delete(reverse("api-part-detail", kwargs={"pk": 1}))
+        response = self.delete(reverse('api-part-detail', kwargs={'pk': 1}))
         self.assertEqual(response.status_code, 204)
 
         with self.assertRaises(Part.DoesNotExist):
@@ -2588,7 +2588,7 @@ class PartStocktakeTest(InvenTreeAPITestCase):
             # Initially no stocktake information available
             self.assertIsNone(p.latest_stocktake)
 
-            note = f"Note {p.pk}"
+            note = f'Note {p.pk}'
             quantity = p.pk + 5
 
             self.post(
diff --git a/InvenTree/part/test_bom_import.py b/InvenTree/part/test_bom_import.py
index 0a4a85fc21..2a1c03d78c 100644
--- a/InvenTree/part/test_bom_import.py
+++ b/InvenTree/part/test_bom_import.py
@@ -33,9 +33,9 @@ class BomUploadTest(InvenTreeAPITestCase):
         for i in range(10):
             parts.append(
                 Part(
-                    name=f"Component {i}",
-                    IPN=f"CMP_{i}",
-                    description="A subcomponent that can be used in a BOM",
+                    name=f'Component {i}',
+                    IPN=f'CMP_{i}',
+                    description='A subcomponent that can be used in a BOM',
                     component=True,
                     assembly=False,
                     lft=0,
diff --git a/InvenTree/part/test_bom_item.py b/InvenTree/part/test_bom_item.py
index 543c576357..4c2a7b7bb5 100644
--- a/InvenTree/part/test_bom_item.py
+++ b/InvenTree/part/test_bom_item.py
@@ -70,7 +70,7 @@ class BomItemTest(TestCase):
     def test_integer_quantity(self):
         """Test integer validation for BomItem."""
         p = Part.objects.create(
-            name="test", description="part description", component=True, trackable=True
+            name='test', description='part description', component=True, trackable=True
         )
 
         # Creation of a BOMItem with a non-integer quantity of a trackable Part should fail
@@ -157,8 +157,8 @@ class BomItemTest(TestCase):
         for ii in range(5):
             # Create a new part
             sub_part = Part.objects.create(
-                name=f"Orphan {ii}",
-                description="A substitute part for the orphan part",
+                name=f'Orphan {ii}',
+                description='A substitute part for the orphan part',
                 component=True,
                 is_template=False,
                 assembly=False,
@@ -196,7 +196,7 @@ class BomItemTest(TestCase):
         """Tests for the 'consumable' BomItem field"""
         # Create an assembly part
         assembly = Part.objects.create(
-            name="An assembly", description="Made with parts", assembly=True
+            name='An assembly', description='Made with parts', assembly=True
         )
 
         # No BOM information initially
@@ -204,16 +204,16 @@ class BomItemTest(TestCase):
 
         # Create some component items
         c1 = Part.objects.create(
-            name="C1", description="Part C1 - this is just the part description"
+            name='C1', description='Part C1 - this is just the part description'
         )
         c2 = Part.objects.create(
-            name="C2", description="Part C2 - this is just the part description"
+            name='C2', description='Part C2 - this is just the part description'
         )
         c3 = Part.objects.create(
-            name="C3", description="Part C3 - this is just the part description"
+            name='C3', description='Part C3 - this is just the part description'
         )
         c4 = Part.objects.create(
-            name="C4", description="Part C4 - this is just the part description"
+            name='C4', description='Part C4 - this is just the part description'
         )
 
         for p in [c1, c2, c3, c4]:
@@ -261,20 +261,20 @@ class BomItemTest(TestCase):
         # Second test: A recursive BOM
         part_a = Part.objects.create(
             name='Part A',
-            description="A part which is called A",
+            description='A part which is called A',
             assembly=True,
             is_template=True,
             component=True,
         )
         part_b = Part.objects.create(
             name='Part B',
-            description="A part which is called B",
+            description='A part which is called B',
             assembly=True,
             component=True,
         )
         part_c = Part.objects.create(
             name='Part C',
-            description="A part which is called C",
+            description='A part which is called C',
             assembly=True,
             component=True,
         )
diff --git a/InvenTree/part/test_category.py b/InvenTree/part/test_category.py
index be29cae943..bef67c71e2 100644
--- a/InvenTree/part/test_category.py
+++ b/InvenTree/part/test_category.py
@@ -106,7 +106,7 @@ class CategoryTest(TestCase):
             letter = chr(ord('A') + idx)
 
             child = PartCategory.objects.create(
-                name=letter * 10, description=f"Subcategory {letter}", parent=parent
+                name=letter * 10, description=f'Subcategory {letter}', parent=parent
             )
 
             parent = child
@@ -114,7 +114,7 @@ class CategoryTest(TestCase):
         self.assertTrue(len(child.path), 26)
         self.assertEqual(
             child.pathstring,
-            "Cat/AAAAAAAAAA/BBBBBBBBBB/CCCCCCCCCC/DDDDDDDDDD/EEEEEEEEEE/FFFFFFFFFF/GGGGGGGGGG/HHHHHHHHHH/IIIIIIIIII/JJJJJJJJJJ/KKKKKKKKK...OO/PPPPPPPPPP/QQQQQQQQQQ/RRRRRRRRRR/SSSSSSSSSS/TTTTTTTTTT/UUUUUUUUUU/VVVVVVVVVV/WWWWWWWWWW/XXXXXXXXXX/YYYYYYYYYY/ZZZZZZZZZZ",
+            'Cat/AAAAAAAAAA/BBBBBBBBBB/CCCCCCCCCC/DDDDDDDDDD/EEEEEEEEEE/FFFFFFFFFF/GGGGGGGGGG/HHHHHHHHHH/IIIIIIIIII/JJJJJJJJJJ/KKKKKKKKK...OO/PPPPPPPPPP/QQQQQQQQQQ/RRRRRRRRRR/SSSSSSSSSS/TTTTTTTTTT/UUUUUUUUUU/VVVVVVVVVV/WWWWWWWWWW/XXXXXXXXXX/YYYYYYYYYY/ZZZZZZZZZZ',
         )
         self.assertTrue(len(child.pathstring) <= 250)
 
diff --git a/InvenTree/part/test_migrations.py b/InvenTree/part/test_migrations.py
index 3bfb3087b6..cbff8e28f9 100644
--- a/InvenTree/part/test_migrations.py
+++ b/InvenTree/part/test_migrations.py
@@ -45,7 +45,7 @@ class TestForwardMigrations(MigratorTestCase):
 
         for name in ['A', 'C', 'E']:
             part = Part.objects.get(name=name)
-            self.assertEqual(part.description, f"My part {name}")
+            self.assertEqual(part.description, f'My part {name}')
 
 
 class TestBomItemMigrations(MigratorTestCase):
diff --git a/InvenTree/part/test_param.py b/InvenTree/part/test_param.py
index 528dfda299..c6f6639353 100644
--- a/InvenTree/part/test_param.py
+++ b/InvenTree/part/test_param.py
@@ -181,7 +181,7 @@ class ParameterTests(TestCase):
         template2 = PartParameterTemplate.objects.create(
             name='My Template 2', units='%'
         )
-        for value in ["1", "1%", "1 percent"]:
+        for value in ['1', '1%', '1 percent']:
             param = PartParameter(part=prt, template=template2, data=value)
             param.full_clean()
 
diff --git a/InvenTree/part/test_part.py b/InvenTree/part/test_part.py
index e74d2d08f5..30da66c7f5 100644
--- a/InvenTree/part/test_part.py
+++ b/InvenTree/part/test_part.py
@@ -180,7 +180,7 @@ class PartTest(TestCase):
     def test_str(self):
         """Test string representation of a Part"""
         p = Part.objects.get(pk=100)
-        self.assertEqual(str(p), "BOB | Bob | A2 - Can we build it? Yes we can!")
+        self.assertEqual(str(p), 'BOB | Bob | A2 - Can we build it? Yes we can!')
 
     def test_duplicate(self):
         """Test that we cannot create a "duplicate" Part."""
diff --git a/InvenTree/part/test_pricing.py b/InvenTree/part/test_pricing.py
index 5d0aac2bf4..a434c3c233 100644
--- a/InvenTree/part/test_pricing.py
+++ b/InvenTree/part/test_pricing.py
@@ -258,8 +258,8 @@ class PartPricingTests(InvenTreeTestCase):
         for ii in range(10):
             # Create a new part for the BOM
             sub_part = part.models.Part.objects.create(
-                name=f"Sub Part {ii}",
-                description="A sub part for use in a BOM",
+                name=f'Sub Part {ii}',
+                description='A sub part for use in a BOM',
                 component=True,
                 assembly=False,
             )
@@ -403,7 +403,7 @@ class PartPricingTests(InvenTreeTestCase):
         # Create some parts
         for ii in range(100):
             part.models.Part.objects.create(
-                name=f"Part_{ii}", description="A test part"
+                name=f'Part_{ii}', description='A test part'
             )
 
         # Ensure there is no pricing data
@@ -424,7 +424,7 @@ class PartPricingTests(InvenTreeTestCase):
         but it pointed to a Part instance which was slated to be deleted inside an atomic transaction.
         """
         p = part.models.Part.objects.create(
-            name="my part", description="my part description", active=False
+            name='my part', description='my part description', active=False
         )
 
         # Create some stock items
diff --git a/InvenTree/part/views.py b/InvenTree/part/views.py
index fb116eb6d5..44993db71b 100644
--- a/InvenTree/part/views.py
+++ b/InvenTree/part/views.py
@@ -105,9 +105,9 @@ class PartImport(FileManagementFormView):
         'part/import_wizard/match_references.html',
     ]
     form_steps_description = [
-        _("Upload File"),
-        _("Match Fields"),
-        _("Match References"),
+        _('Upload File'),
+        _('Match Fields'),
+        _('Match References'),
     ]
 
     form_field_map = {
@@ -540,8 +540,8 @@ class PartPricing(AjaxView):
     """View for inspecting part pricing information."""
 
     model = Part
-    ajax_template_name = "part/part_pricing.html"
-    ajax_form_title = _("Part Pricing")
+    ajax_template_name = 'part/part_pricing.html'
+    ajax_form_title = _('Part Pricing')
     form_class = part_forms.PartPriceForm
 
     role_required = ['sales_order.view', 'part.view']
diff --git a/InvenTree/plugin/admin.py b/InvenTree/plugin/admin.py
index 23bee2fcc5..102a599da9 100644
--- a/InvenTree/plugin/admin.py
+++ b/InvenTree/plugin/admin.py
@@ -49,7 +49,7 @@ class PluginSettingInline(admin.TabularInline):
 class PluginConfigAdmin(admin.ModelAdmin):
     """Custom admin with restricted id fields."""
 
-    readonly_fields = ["key", "name"]
+    readonly_fields = ['key', 'name']
     list_display = [
         'name',
         'key',
diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py
index e2fbf99a45..8f5d7de655 100644
--- a/InvenTree/plugin/api.py
+++ b/InvenTree/plugin/api.py
@@ -227,7 +227,7 @@ def check_plugin(plugin_slug: str, plugin_pk: int) -> InvenTreePlugin:
     """
     # Make sure that a plugin reference is specified
     if plugin_slug is None and plugin_pk is None:
-        raise NotFound(detail="Plugin not specified")
+        raise NotFound(detail='Plugin not specified')
 
     # Define filter
     filter = {}
@@ -342,13 +342,13 @@ class RegistryStatusView(APIView):
             for error_detail in errors:
                 for name, message in error_detail.items():
                     error_list.append({
-                        "stage": stage,
-                        "name": name,
-                        "message": message,
+                        'stage': stage,
+                        'name': name,
+                        'message': message,
                     })
 
         result = PluginSerializers.PluginRegistryStatusSerializer({
-            "registry_errors": error_list
+            'registry_errors': error_list
         }).data
 
         return Response(result)
@@ -382,7 +382,7 @@ plugin_api_urls = [
                 r'<int:pk>/',
                 include([
                     re_path(
-                        r"^settings/",
+                        r'^settings/',
                         include([
                             re_path(
                                 r'^(?P<key>\w+)/',
@@ -390,9 +390,9 @@ plugin_api_urls = [
                                 name='api-plugin-setting-detail-pk',
                             ),
                             re_path(
-                                r"^.*$",
+                                r'^.*$',
                                 PluginAllSettingList.as_view(),
-                                name="api-plugin-settings",
+                                name='api-plugin-settings',
                             ),
                         ]),
                     ),
@@ -419,9 +419,9 @@ plugin_api_urls = [
             ),
             # Registry status
             re_path(
-                r"^status/",
+                r'^status/',
                 RegistryStatusView.as_view(),
-                name="api-plugin-registry-status",
+                name='api-plugin-registry-status',
             ),
             # Anything else
             re_path(r'^.*$', PluginList.as_view(), name='api-plugin-list'),
diff --git a/InvenTree/plugin/apps.py b/InvenTree/plugin/apps.py
index 24bc34ab1f..b617dcd1fe 100644
--- a/InvenTree/plugin/apps.py
+++ b/InvenTree/plugin/apps.py
@@ -28,7 +28,7 @@ class PluginAppConfig(AppConfig):
             return
 
         if not canAppAccessDatabase(allow_test=True, allow_plugins=True):
-            logger.info("Skipping plugin loading sequence")  # pragma: no cover
+            logger.info('Skipping plugin loading sequence')  # pragma: no cover
         else:
             logger.info('Loading InvenTree plugins')
 
diff --git a/InvenTree/plugin/base/action/api.py b/InvenTree/plugin/base/action/api.py
index d2ee877fff..37dabd9eda 100644
--- a/InvenTree/plugin/base/action/api.py
+++ b/InvenTree/plugin/base/action/api.py
@@ -21,7 +21,7 @@ class ActionPluginView(APIView):
         data = request.data.get('data', None)
 
         if action is None:
-            return Response({'error': _("No action specified")})
+            return Response({'error': _('No action specified')})
 
         action_plugins = registry.with_mixin('action')
         for plugin in action_plugins:
@@ -30,4 +30,4 @@ class ActionPluginView(APIView):
                 return Response(plugin.get_response(request.user, data=data))
 
         # If we got to here, no matching action was found
-        return Response({'error': _("No matching action found"), "action": action})
+        return Response({'error': _('No matching action found'), 'action': action})
diff --git a/InvenTree/plugin/base/action/mixins.py b/InvenTree/plugin/base/action/mixins.py
index b978a69189..e4931c1012 100644
--- a/InvenTree/plugin/base/action/mixins.py
+++ b/InvenTree/plugin/base/action/mixins.py
@@ -4,7 +4,7 @@
 class ActionMixin:
     """Mixin that enables custom actions."""
 
-    ACTION_NAME = ""
+    ACTION_NAME = ''
 
     class MixinMeta:
         """Meta options for this mixin."""
@@ -47,7 +47,7 @@ class ActionMixin:
         Default implementation is a simple response which can be overridden.
         """
         return {
-            "action": self.action_name(),
-            "result": self.get_result(user, data),
-            "info": self.get_info(user, data),
+            'action': self.action_name(),
+            'result': self.get_result(user, data),
+            'info': self.get_info(user, data),
         }
diff --git a/InvenTree/plugin/base/action/test_action.py b/InvenTree/plugin/base/action/test_action.py
index 322779cf8b..c2bcf6e3ec 100644
--- a/InvenTree/plugin/base/action/test_action.py
+++ b/InvenTree/plugin/base/action/test_action.py
@@ -57,7 +57,7 @@ class ActionMixinTests(TestCase):
         self.assertEqual(self.plugin.get_result(), False)
         self.assertIsNone(self.plugin.get_info())
         self.assertEqual(
-            self.plugin.get_response(), {"action": '', "result": False, "info": None}
+            self.plugin.get_response(), {'action': '', 'result': False, 'info': None}
         )
 
         # overridden functions
@@ -69,9 +69,9 @@ class ActionMixinTests(TestCase):
         self.assertEqual(
             self.action_plugin.get_response(),
             {
-                "action": 'abc123',
-                "result": self.ACTION_RETURN + 'result',
-                "info": self.ACTION_RETURN + 'info',
+                'action': 'abc123',
+                'result': self.ACTION_RETURN + 'result',
+                'info': self.ACTION_RETURN + 'info',
             },
         )
 
@@ -87,7 +87,7 @@ class APITests(InvenTreeTestCase):
         self.assertEqual(response.data, {'error': 'No action specified'})
 
         # Test non-exsisting action
-        response = self.client.post('/api/action/', data={'action': "nonexsisting"})
+        response = self.client.post('/api/action/', data={'action': 'nonexsisting'})
         self.assertEqual(response.status_code, 200)
         self.assertEqual(
             response.data,
diff --git a/InvenTree/plugin/base/barcodes/api.py b/InvenTree/plugin/base/barcodes/api.py
index e6021b7ebf..3a829b9752 100644
--- a/InvenTree/plugin/base/barcodes/api.py
+++ b/InvenTree/plugin/base/barcodes/api.py
@@ -58,7 +58,7 @@ class BarcodeView(CreateAPIView):
             Any custom fields passed by the specific serializer
         """
         raise NotImplementedError(
-            f"handle_barcode not implemented for {self.__class__}"
+            f'handle_barcode not implemented for {self.__class__}'
         )
 
     def scan_barcode(self, barcode: str, request, **kwargs):
@@ -79,11 +79,11 @@ class BarcodeView(CreateAPIView):
             if result is None:
                 continue
 
-            if "error" in result:
+            if 'error' in result:
                 logger.info(
-                    "%s.scan(...) returned an error: %s",
+                    '%s.scan(...) returned an error: %s',
                     current_plugin.__class__.__name__,
-                    result["error"],
+                    result['error'],
                 )
                 if not response:
                     plugin = current_plugin
@@ -155,9 +155,9 @@ class BarcodeAssign(BarcodeView):
             result = plugin.scan(barcode)
 
             if result is not None:
-                result["error"] = _("Barcode matches existing item")
-                result["plugin"] = plugin.name
-                result["barcode_data"] = barcode
+                result['error'] = _('Barcode matches existing item')
+                result['plugin'] = plugin.name
+                result['barcode_data'] = barcode
 
                 raise ValidationError(result)
 
@@ -174,20 +174,20 @@ class BarcodeAssign(BarcodeView):
                 app_label = model._meta.app_label
                 model_name = model._meta.model_name
 
-                table = f"{app_label}_{model_name}"
+                table = f'{app_label}_{model_name}'
 
-                if not RuleSet.check_table_permission(request.user, table, "change"):
+                if not RuleSet.check_table_permission(request.user, table, 'change'):
                     raise PermissionDenied({
-                        "error": f"You do not have the required permissions for {table}"
+                        'error': f'You do not have the required permissions for {table}'
                     })
 
                 instance.assign_barcode(barcode_data=barcode, barcode_hash=barcode_hash)
 
                 return Response({
-                    'success': f"Assigned barcode to {label} instance",
+                    'success': f'Assigned barcode to {label} instance',
                     label: {'pk': instance.pk},
-                    "barcode_data": barcode,
-                    "barcode_hash": barcode_hash,
+                    'barcode_data': barcode,
+                    'barcode_hash': barcode_hash,
                 })
 
         # If we got here, it means that no valid model types were provided
@@ -238,11 +238,11 @@ class BarcodeUnassign(BarcodeView):
                 app_label = model._meta.app_label
                 model_name = model._meta.model_name
 
-                table = f"{app_label}_{model_name}"
+                table = f'{app_label}_{model_name}'
 
-                if not RuleSet.check_table_permission(request.user, table, "change"):
+                if not RuleSet.check_table_permission(request.user, table, 'change'):
                     raise PermissionDenied({
-                        "error": f"You do not have the required permissions for {table}"
+                        'error': f'You do not have the required permissions for {table}'
                     })
 
                 # Unassign the barcode data from the model instance
@@ -313,11 +313,11 @@ class BarcodePOAllocate(BarcodeView):
                 )
 
         if supplier_parts.count() == 0:
-            raise ValidationError({"error": _("No matching supplier parts found")})
+            raise ValidationError({'error': _('No matching supplier parts found')})
 
         if supplier_parts.count() > 1:
             raise ValidationError({
-                "error": _("Multiple matching supplier parts found")
+                'error': _('Multiple matching supplier parts found')
             })
 
         # At this stage, we have a single matching supplier part
@@ -342,7 +342,7 @@ class BarcodePOAllocate(BarcodeView):
             manufacturer_part=result.get('manufacturerpart', None),
         )
 
-        result['success'] = _("Matched supplier part")
+        result['success'] = _('Matched supplier part')
         result['supplierpart'] = supplier_part.format_matched_response()
 
         # TODO: Determine the 'quantity to order' for the supplier part
@@ -379,24 +379,24 @@ class BarcodePOReceive(BarcodeView):
         purchase_order = kwargs.get('purchase_order', None)
         location = kwargs.get('location', None)
 
-        plugins = registry.with_mixin("barcode")
+        plugins = registry.with_mixin('barcode')
 
         # Look for a barcode plugin which knows how to deal with this barcode
         plugin = None
 
-        response = {"barcode_data": barcode, "barcode_hash": hash_barcode(barcode)}
+        response = {'barcode_data': barcode, 'barcode_hash': hash_barcode(barcode)}
 
         internal_barcode_plugin = next(
-            filter(lambda plugin: plugin.name == "InvenTreeBarcode", plugins)
+            filter(lambda plugin: plugin.name == 'InvenTreeBarcode', plugins)
         )
 
         if result := internal_barcode_plugin.scan(barcode):
             if 'stockitem' in result:
-                response["error"] = _("Item has already been received")
+                response['error'] = _('Item has already been received')
                 raise ValidationError(response)
 
         # Now, look just for "supplier-barcode" plugins
-        plugins = registry.with_mixin("supplier-barcode")
+        plugins = registry.with_mixin('supplier-barcode')
 
         plugin_response = None
 
@@ -408,11 +408,11 @@ class BarcodePOReceive(BarcodeView):
             if result is None:
                 continue
 
-            if "error" in result:
+            if 'error' in result:
                 logger.info(
-                    "%s.scan_receive_item(...) returned an error: %s",
+                    '%s.scan_receive_item(...) returned an error: %s',
                     current_plugin.__class__.__name__,
-                    result["error"],
+                    result['error'],
                 )
                 if not plugin_response:
                     plugin = current_plugin
@@ -429,9 +429,9 @@ class BarcodePOReceive(BarcodeView):
 
         # A plugin has not been found!
         if plugin is None:
-            response["error"] = _("No match for supplier barcode")
+            response['error'] = _('No match for supplier barcode')
             raise ValidationError(response)
-        elif "error" in response:
+        elif 'error' in response:
             raise ValidationError(response)
         else:
             return Response(response)
@@ -583,11 +583,11 @@ barcode_api_urls = [
     # Unlink a third-party barcode from an item
     path('unlink/', BarcodeUnassign.as_view(), name='api-barcode-unlink'),
     # Receive a purchase order item by scanning its barcode
-    path("po-receive/", BarcodePOReceive.as_view(), name="api-barcode-po-receive"),
+    path('po-receive/', BarcodePOReceive.as_view(), name='api-barcode-po-receive'),
     # Allocate parts to a purchase order by scanning their barcode
-    path("po-allocate/", BarcodePOAllocate.as_view(), name="api-barcode-po-allocate"),
+    path('po-allocate/', BarcodePOAllocate.as_view(), name='api-barcode-po-allocate'),
     # Allocate stock to a sales order by scanning barcode
-    path("so-allocate/", BarcodeSOAllocate.as_view(), name="api-barcode-so-allocate"),
+    path('so-allocate/', BarcodeSOAllocate.as_view(), name='api-barcode-so-allocate'),
     # Catch-all performs barcode 'scan'
     re_path(r'^.*$', BarcodeScan.as_view(), name='api-barcode-scan'),
 ]
diff --git a/InvenTree/plugin/base/barcodes/mixins.py b/InvenTree/plugin/base/barcodes/mixins.py
index 7574d86abd..904b40c9f0 100644
--- a/InvenTree/plugin/base/barcodes/mixins.py
+++ b/InvenTree/plugin/base/barcodes/mixins.py
@@ -23,7 +23,7 @@ class BarcodeMixin:
     Custom barcode plugins should use and extend this mixin as necessary.
     """
 
-    ACTION_NAME = ""
+    ACTION_NAME = ''
 
     class MixinMeta:
         """Meta options for this mixin."""
@@ -62,19 +62,19 @@ class SupplierBarcodeMixin(BarcodeMixin):
     """
 
     # Set of standard field names which can be extracted from the barcode
-    CUSTOMER_ORDER_NUMBER = "customer_order_number"
-    SUPPLIER_ORDER_NUMBER = "supplier_order_number"
-    PACKING_LIST_NUMBER = "packing_list_number"
-    SHIP_DATE = "ship_date"
-    CUSTOMER_PART_NUMBER = "customer_part_number"
-    SUPPLIER_PART_NUMBER = "supplier_part_number"
-    PURCHASE_ORDER_LINE = "purchase_order_line"
-    QUANTITY = "quantity"
-    DATE_CODE = "date_code"
-    LOT_CODE = "lot_code"
-    COUNTRY_OF_ORIGIN = "country_of_origin"
-    MANUFACTURER = "manufacturer"
-    MANUFACTURER_PART_NUMBER = "manufacturer_part_number"
+    CUSTOMER_ORDER_NUMBER = 'customer_order_number'
+    SUPPLIER_ORDER_NUMBER = 'supplier_order_number'
+    PACKING_LIST_NUMBER = 'packing_list_number'
+    SHIP_DATE = 'ship_date'
+    CUSTOMER_PART_NUMBER = 'customer_part_number'
+    SUPPLIER_PART_NUMBER = 'supplier_part_number'
+    PURCHASE_ORDER_LINE = 'purchase_order_line'
+    QUANTITY = 'quantity'
+    DATE_CODE = 'date_code'
+    LOT_CODE = 'lot_code'
+    COUNTRY_OF_ORIGIN = 'country_of_origin'
+    MANUFACTURER = 'manufacturer'
+    MANUFACTURER_PART_NUMBER = 'manufacturer_part_number'
 
     def __init__(self):
         """Register mixin."""
@@ -83,7 +83,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
 
     def get_field_value(self, key, backup_value=None):
         """Return the value of a barcode field."""
-        fields = getattr(self, "barcode_fields", None) or {}
+        fields = getattr(self, 'barcode_fields', None) or {}
 
         return fields.get(key, backup_value)
 
@@ -125,7 +125,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
 
         """
         raise NotImplementedError(
-            "extract_barcode_fields must be implemented by each plugin"
+            'extract_barcode_fields must be implemented by each plugin'
         )
 
     def scan(self, barcode_data):
@@ -145,16 +145,16 @@ class SupplierBarcodeMixin(BarcodeMixin):
         )
 
         if len(supplier_parts) > 1:
-            return {"error": _("Found multiple matching supplier parts for barcode")}
+            return {'error': _('Found multiple matching supplier parts for barcode')}
         elif not supplier_parts:
             return None
 
         supplier_part = supplier_parts[0]
 
         data = {
-            "pk": supplier_part.pk,
-            "api_url": f"{SupplierPart.get_api_url()}{supplier_part.pk}/",
-            "web_url": supplier_part.get_absolute_url(),
+            'pk': supplier_part.pk,
+            'api_url': f'{SupplierPart.get_api_url()}{supplier_part.pk}/',
+            'web_url': supplier_part.get_absolute_url(),
         }
 
         return {SupplierPart.barcode_model_type(): data}
@@ -178,7 +178,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
         )
 
         if len(supplier_parts) > 1:
-            return {"error": _("Found multiple matching supplier parts for barcode")}
+            return {'error': _('Found multiple matching supplier parts for barcode')}
         elif not supplier_parts:
             return None
 
@@ -196,17 +196,17 @@ class SupplierBarcodeMixin(BarcodeMixin):
 
             if len(matching_orders) > 1:
                 return {
-                    "error": _(f"Found multiple purchase orders matching '{order}'")
+                    'error': _(f"Found multiple purchase orders matching '{order}'")
                 }
 
             if len(matching_orders) == 0:
-                return {"error": _(f"No matching purchase order for '{order}'")}
+                return {'error': _(f"No matching purchase order for '{order}'")}
 
             purchase_order = matching_orders.first()
 
         if supplier and purchase_order:
             if purchase_order.supplier != supplier:
-                return {"error": _("Purchase order does not match supplier")}
+                return {'error': _('Purchase order does not match supplier')}
 
         return self.receive_purchase_order_item(
             supplier_part,
@@ -226,17 +226,17 @@ class SupplierBarcodeMixin(BarcodeMixin):
         if not isinstance(self, SettingsMixin):
             return None
 
-        if supplier_pk := self.get_setting("SUPPLIER_ID"):
+        if supplier_pk := self.get_setting('SUPPLIER_ID'):
             if supplier := Company.objects.get(pk=supplier_pk):
                 return supplier
             else:
                 logger.error(
-                    "No company with pk %d (set \"SUPPLIER_ID\" setting to a valid value)",
+                    'No company with pk %d (set "SUPPLIER_ID" setting to a valid value)',
                     supplier_pk,
                 )
                 return None
 
-        if not (supplier_name := getattr(self, "DEFAULT_SUPPLIER_NAME", None)):
+        if not (supplier_name := getattr(self, 'DEFAULT_SUPPLIER_NAME', None)):
             return None
 
         suppliers = Company.objects.filter(
@@ -246,7 +246,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
         if len(suppliers) != 1:
             return None
 
-        self.set_setting("SUPPLIER_ID", suppliers.first().pk)
+        self.set_setting('SUPPLIER_ID', suppliers.first().pk)
 
         return suppliers.first()
 
@@ -260,21 +260,21 @@ class SupplierBarcodeMixin(BarcodeMixin):
         if it does not use the standard field names.
         """
         return {
-            "K": cls.CUSTOMER_ORDER_NUMBER,
-            "1K": cls.SUPPLIER_ORDER_NUMBER,
-            "11K": cls.PACKING_LIST_NUMBER,
-            "6D": cls.SHIP_DATE,
-            "9D": cls.DATE_CODE,
-            "10D": cls.DATE_CODE,
-            "4K": cls.PURCHASE_ORDER_LINE,
-            "14K": cls.PURCHASE_ORDER_LINE,
-            "P": cls.SUPPLIER_PART_NUMBER,
-            "1P": cls.MANUFACTURER_PART_NUMBER,
-            "30P": cls.SUPPLIER_PART_NUMBER,
-            "1T": cls.LOT_CODE,
-            "4L": cls.COUNTRY_OF_ORIGIN,
-            "1V": cls.MANUFACTURER,
-            "Q": cls.QUANTITY,
+            'K': cls.CUSTOMER_ORDER_NUMBER,
+            '1K': cls.SUPPLIER_ORDER_NUMBER,
+            '11K': cls.PACKING_LIST_NUMBER,
+            '6D': cls.SHIP_DATE,
+            '9D': cls.DATE_CODE,
+            '10D': cls.DATE_CODE,
+            '4K': cls.PURCHASE_ORDER_LINE,
+            '14K': cls.PURCHASE_ORDER_LINE,
+            'P': cls.SUPPLIER_PART_NUMBER,
+            '1P': cls.MANUFACTURER_PART_NUMBER,
+            '30P': cls.SUPPLIER_PART_NUMBER,
+            '1T': cls.LOT_CODE,
+            '4L': cls.COUNTRY_OF_ORIGIN,
+            '1V': cls.MANUFACTURER,
+            'Q': cls.QUANTITY,
         }
 
     @classmethod
@@ -324,10 +324,10 @@ class SupplierBarcodeMixin(BarcodeMixin):
     def parse_isoiec_15434_barcode2d(barcode_data: str) -> list[str]:
         """Parse a ISO/IEC 15434 barcode, returning the split data section."""
 
-        OLD_MOUSER_HEADER = ">[)>06\x1d"
-        HEADER = "[)>\x1e06\x1d"
-        TRAILER = "\x1e\x04"
-        DELIMITER = "\x1d"
+        OLD_MOUSER_HEADER = '>[)>06\x1d'
+        HEADER = '[)>\x1e06\x1d'
+        TRAILER = '\x1e\x04'
+        DELIMITER = '\x1d'
 
         # Some old mouser barcodes start with this messed up header
         if barcode_data.startswith(OLD_MOUSER_HEADER):
@@ -419,7 +419,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
 
         #  find incomplete line_items that match the supplier_part
         line_items = purchase_order.lines.filter(
-            part=supplier_part.pk, quantity__gt=F("received")
+            part=supplier_part.pk, quantity__gt=F('received')
         )
         if len(line_items) == 1 or not quantity:
             line_item = line_items[0]
@@ -439,7 +439,7 @@ class SupplierBarcodeMixin(BarcodeMixin):
                     line_item = line_items.first()
 
         if not line_item:
-            return {"error": _("Failed to find pending line item for supplier part")}
+            return {'error': _('Failed to find pending line item for supplier part')}
 
         no_stock_locations = False
         if not location:
@@ -457,20 +457,20 @@ class SupplierBarcodeMixin(BarcodeMixin):
                     no_stock_locations = True
 
         response = {
-            "lineitem": {"pk": line_item.pk, "purchase_order": purchase_order.pk}
+            'lineitem': {'pk': line_item.pk, 'purchase_order': purchase_order.pk}
         }
 
         if quantity:
-            response["lineitem"]["quantity"] = quantity
+            response['lineitem']['quantity'] = quantity
         if location:
-            response["lineitem"]["location"] = location.pk
+            response['lineitem']['location'] = location.pk
 
         # if either the quantity is missing or no location is defined/found
         # -> return the line_item found, so the client can gather the missing
         #    information and complete the action with an 'api-po-receive' call
         if not quantity or (not location and not no_stock_locations):
-            response["action_required"] = _(
-                "Further information required to receive line item"
+            response['action_required'] = _(
+                'Further information required to receive line item'
             )
             return response
 
@@ -478,5 +478,5 @@ class SupplierBarcodeMixin(BarcodeMixin):
             line_item, location, quantity, user, barcode=barcode
         )
 
-        response["success"] = _("Received purchase order line item")
+        response['success'] = _('Received purchase order line item')
         return response
diff --git a/InvenTree/plugin/base/barcodes/serializers.py b/InvenTree/plugin/base/barcodes/serializers.py
index 52c4f83ecb..9482969545 100644
--- a/InvenTree/plugin/base/barcodes/serializers.py
+++ b/InvenTree/plugin/base/barcodes/serializers.py
@@ -86,7 +86,7 @@ class BarcodePOAllocateSerializer(BarcodeSerializer):
         """Validate the provided order"""
 
         if order.status != PurchaseOrderStatus.PENDING.value:
-            raise ValidationError(_("Purchase order is not pending"))
+            raise ValidationError(_('Purchase order is not pending'))
 
         return order
 
@@ -111,7 +111,7 @@ class BarcodePOReceiveSerializer(BarcodeSerializer):
         """Validate the provided order"""
 
         if order and order.status != PurchaseOrderStatus.PLACED.value:
-            raise ValidationError(_("Purchase order has not been placed"))
+            raise ValidationError(_('Purchase order has not been placed'))
 
         return order
 
@@ -126,7 +126,7 @@ class BarcodePOReceiveSerializer(BarcodeSerializer):
         """Validate the provided location"""
 
         if location and location.structural:
-            raise ValidationError(_("Cannot select a structural location"))
+            raise ValidationError(_('Cannot select a structural location'))
 
         return location
 
@@ -147,7 +147,7 @@ class BarcodeSOAllocateSerializer(BarcodeSerializer):
         """Validate the provided order"""
 
         if order and order.status != SalesOrderStatus.PENDING.value:
-            raise ValidationError(_("Sales order is not pending"))
+            raise ValidationError(_('Sales order is not pending'))
 
         return order
 
@@ -169,7 +169,7 @@ class BarcodeSOAllocateSerializer(BarcodeSerializer):
         """Validate the provided shipment"""
 
         if shipment and shipment.is_delivered():
-            raise ValidationError(_("Shipment has already been delivered"))
+            raise ValidationError(_('Shipment has already been delivered'))
 
         return shipment
 
diff --git a/InvenTree/plugin/base/barcodes/test_barcode.py b/InvenTree/plugin/base/barcodes/test_barcode.py
index c75229d279..3b5cedba6f 100644
--- a/InvenTree/plugin/base/barcodes/test_barcode.py
+++ b/InvenTree/plugin/base/barcodes/test_barcode.py
@@ -213,7 +213,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
         for k in invalid_keys:
             response = self.post(self.unassign_url, {k: 123}, expected_code=400)
 
-            self.assertIn("Missing data: Provide one of", str(response.data['error']))
+            self.assertIn('Missing data: Provide one of', str(response.data['error']))
 
         valid_keys = ['build', 'salesorder', 'part']
 
@@ -221,7 +221,7 @@ class BarcodeAPITest(InvenTreeAPITestCase):
         for k in valid_keys:
             response = self.post(self.unassign_url, {k: 999999999}, expected_code=400)
 
-            self.assertIn("object does not exist", str(response.data[k]))
+            self.assertIn('object does not exist', str(response.data[k]))
 
 
 class SOAllocateTest(InvenTreeAPITestCase):
diff --git a/InvenTree/plugin/base/integration/APICallMixin.py b/InvenTree/plugin/base/integration/APICallMixin.py
index 11bead3327..8be6218a32 100644
--- a/InvenTree/plugin/base/integration/APICallMixin.py
+++ b/InvenTree/plugin/base/integration/APICallMixin.py
@@ -76,9 +76,9 @@ class APICallMixin:
     def has_api_call(self):
         """Is the mixin ready to call external APIs?"""
         if not bool(self.API_URL_SETTING):
-            raise MixinNotImplementedError("API_URL_SETTING must be defined")
+            raise MixinNotImplementedError('API_URL_SETTING must be defined')
         if not bool(self.API_TOKEN_SETTING):
-            raise MixinNotImplementedError("API_TOKEN_SETTING must be defined")
+            raise MixinNotImplementedError('API_TOKEN_SETTING must be defined')
         return True
 
     @property
@@ -99,7 +99,7 @@ class APICallMixin:
 
             if token:
                 headers[self.API_TOKEN] = token
-                headers['Authorization'] = f"{self.API_TOKEN} {token}"
+                headers['Authorization'] = f'{self.API_TOKEN} {token}'
 
         return headers
 
diff --git a/InvenTree/plugin/base/integration/CurrencyExchangeMixin.py b/InvenTree/plugin/base/integration/CurrencyExchangeMixin.py
index 613b54d8ad..670e2efeb1 100644
--- a/InvenTree/plugin/base/integration/CurrencyExchangeMixin.py
+++ b/InvenTree/plugin/base/integration/CurrencyExchangeMixin.py
@@ -16,7 +16,7 @@ class CurrencyExchangeMixin:
     class MixinMeta:
         """Meta options for this mixin class"""
 
-        MIXIN_NAME = "CurrentExchange"
+        MIXIN_NAME = 'CurrentExchange'
 
     def __init__(self):
         """Register the mixin"""
@@ -39,5 +39,5 @@ class CurrencyExchangeMixin:
             Can raise any exception if the update fails
         """
         raise MixinNotImplementedError(
-            "Plugin must implement update_exchange_rates method"
+            'Plugin must implement update_exchange_rates method'
         )
diff --git a/InvenTree/plugin/base/integration/ScheduleMixin.py b/InvenTree/plugin/base/integration/ScheduleMixin.py
index edde7a2e4e..3b06c88315 100644
--- a/InvenTree/plugin/base/integration/ScheduleMixin.py
+++ b/InvenTree/plugin/base/integration/ScheduleMixin.py
@@ -76,7 +76,7 @@ class ScheduleMixin:
                         task_keys += plugin.get_task_names()
 
         if len(task_keys) > 0:
-            logger.info("Activated %s scheduled tasks", len(task_keys))
+            logger.info('Activated %s scheduled tasks', len(task_keys))
 
         # Remove any scheduled tasks which do not match
         # This stops 'old' plugin tasks from accumulating
@@ -84,7 +84,7 @@ class ScheduleMixin:
             from django_q.models import Schedule
 
             scheduled_plugin_tasks = Schedule.objects.filter(
-                name__istartswith="plugin."
+                name__istartswith='plugin.'
             )
 
             deleted_count = 0
@@ -96,11 +96,11 @@ class ScheduleMixin:
 
             if deleted_count > 0:
                 logger.info(
-                    "Removed %s old scheduled tasks", deleted_count
+                    'Removed %s old scheduled tasks', deleted_count
                 )  # pragma: no cover
         except (ProgrammingError, OperationalError):
             # Database might not yet be ready
-            logger.warning("activate_integration_schedule failed, database not ready")
+            logger.warning('activate_integration_schedule failed, database not ready')
 
     def get_scheduled_tasks(self):
         """Returns `SCHEDULED_TASKS` context.
@@ -117,7 +117,7 @@ class ScheduleMixin:
     def validate_scheduled_tasks(self):
         """Check that the provided scheduled tasks are valid."""
         if not self.has_scheduled_tasks:
-            raise MixinImplementationError("SCHEDULED_TASKS not defined")
+            raise MixinImplementationError('SCHEDULED_TASKS not defined')
 
         for key, task in self.scheduled_tasks.items():
             if 'func' not in task:
@@ -147,7 +147,7 @@ class ScheduleMixin:
         """Task name for key."""
         # Generate a 'unique' task name
         slug = self.plugin_slug()
-        return f"plugin.{slug}.{key}"
+        return f'plugin.{slug}.{key}'
 
     def get_task_names(self):
         """All defined task names."""
@@ -194,7 +194,7 @@ class ScheduleMixin:
 
         except (ProgrammingError, OperationalError):  # pragma: no cover
             # Database might not yet be ready
-            logger.warning("register_tasks failed, database not ready")
+            logger.warning('register_tasks failed, database not ready')
 
     def unregister_tasks(self):
         """Deregister the tasks with the database."""
@@ -211,4 +211,4 @@ class ScheduleMixin:
                     pass
         except (ProgrammingError, OperationalError):  # pragma: no cover
             # Database might not yet be ready
-            logger.warning("unregister_tasks failed, database not ready")
+            logger.warning('unregister_tasks failed, database not ready')
diff --git a/InvenTree/plugin/base/integration/ValidationMixin.py b/InvenTree/plugin/base/integration/ValidationMixin.py
index 0376984ace..cee5030b70 100644
--- a/InvenTree/plugin/base/integration/ValidationMixin.py
+++ b/InvenTree/plugin/base/integration/ValidationMixin.py
@@ -33,7 +33,7 @@ class ValidationMixin:
     class MixinMeta:
         """Metaclass for this mixin"""
 
-        MIXIN_NAME = "Validation"
+        MIXIN_NAME = 'Validation'
 
     def __init__(self):
         """Register the mixin"""
diff --git a/InvenTree/plugin/base/integration/mixins.py b/InvenTree/plugin/base/integration/mixins.py
index 53e234637e..0421472483 100644
--- a/InvenTree/plugin/base/integration/mixins.py
+++ b/InvenTree/plugin/base/integration/mixins.py
@@ -12,7 +12,7 @@ class NavigationMixin:
     """Mixin that enables custom navigation links with the plugin."""
 
     NAVIGATION_TAB_NAME = None
-    NAVIGATION_TAB_ICON = "fas fa-question"
+    NAVIGATION_TAB_ICON = 'fas fa-question'
 
     class MixinMeta:
         """Meta options for this mixin."""
@@ -51,7 +51,7 @@ class NavigationMixin:
     @property
     def navigation_icon(self):
         """Icon-name for navigation tab."""
-        return getattr(self, 'NAVIGATION_TAB_ICON', "fas fa-question")
+        return getattr(self, 'NAVIGATION_TAB_ICON', 'fas fa-question')
 
 
 class PanelMixin:
@@ -175,7 +175,7 @@ class PanelMixin:
 
             if any(key not in panel for key in required_keys):
                 logger.warning(
-                    "Custom panel for plugin %s is missing a required parameter",
+                    'Custom panel for plugin %s is missing a required parameter',
                     __class__,
                 )
                 continue
diff --git a/InvenTree/plugin/base/integration/test_mixins.py b/InvenTree/plugin/base/integration/test_mixins.py
index 70bd7d41b3..e512fb9cea 100644
--- a/InvenTree/plugin/base/integration/test_mixins.py
+++ b/InvenTree/plugin/base/integration/test_mixins.py
@@ -206,7 +206,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
         """Setup for all tests."""
 
         class MixinCls(APICallMixin, SettingsMixin, InvenTreePlugin):
-            NAME = "Sample API Caller"
+            NAME = 'Sample API Caller'
 
             SETTINGS = {
                 'API_TOKEN': {'name': 'API Token', 'protected': True},
@@ -223,7 +223,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
             @property
             def api_url(self):
                 """Override API URL for this test"""
-                return "https://api.github.com"
+                return 'https://api.github.com'
 
             def get_external_url(self, simple: bool = True):
                 """Returns data from the sample endpoint."""
@@ -289,7 +289,7 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
         # api_call with post and data
         result = self.mixin.api_call(
             'https://reqres.in/api/users/',
-            json={"name": "morpheus", "job": "leader"},
+            json={'name': 'morpheus', 'job': 'leader'},
             method='POST',
             endpoint_is_url=True,
         )
@@ -321,13 +321,13 @@ class APICallMixinTest(BaseMixinDefinition, TestCase):
         # Too many data arguments
         with self.assertRaises(ValueError):
             self.mixin.api_call(
-                'https://reqres.in/api/users/', json={"a": 1}, data={"a": 1}
+                'https://reqres.in/api/users/', json={'a': 1}, data={'a': 1}
             )
 
         # Sending a request with a wrong data format should result in 40
         result = self.mixin.api_call(
             'https://reqres.in/api/users/',
-            data={"name": "morpheus", "job": "leader"},
+            data={'name': 'morpheus', 'job': 'leader'},
             method='POST',
             endpoint_is_url=True,
             simple_response=False,
diff --git a/InvenTree/plugin/base/label/mixins.py b/InvenTree/plugin/base/label/mixins.py
index 4462526816..8c62b0b09f 100644
--- a/InvenTree/plugin/base/label/mixins.py
+++ b/InvenTree/plugin/base/label/mixins.py
@@ -186,7 +186,7 @@ class LabelPrintingMixin:
             A class instance of a DRF serializer class, by default this an instance of
             self.PrintingOptionsSerializer using the *args, **kwargs if existing for this plugin
         """
-        serializer = getattr(self, "PrintingOptionsSerializer", None)
+        serializer = getattr(self, 'PrintingOptionsSerializer', None)
 
         if not serializer:
             return None
diff --git a/InvenTree/plugin/base/label/test_label_mixin.py b/InvenTree/plugin/base/label/test_label_mixin.py
index 17ae265fcd..ea9a3eacb6 100644
--- a/InvenTree/plugin/base/label/test_label_mixin.py
+++ b/InvenTree/plugin/base/label/test_label_mixin.py
@@ -46,7 +46,7 @@ class LabelMixinTests(InvenTreeAPITestCase):
         # Construct URL
         kwargs = {}
         if label:
-            kwargs["pk"] = label.pk
+            kwargs['pk'] = label.pk
 
         url = reverse(url_name, kwargs=kwargs)
 
@@ -133,13 +133,13 @@ class LabelMixinTests(InvenTreeAPITestCase):
         # Non-exsisting plugin
         response = self.get(f'{url}123', expected_code=404)
         self.assertIn(
-            f'Plugin \'{plugin_ref}123\' not found', str(response.content, 'utf8')
+            f"Plugin '{plugin_ref}123' not found", str(response.content, 'utf8')
         )
 
         # Inactive plugin
         response = self.get(url, expected_code=400)
         self.assertIn(
-            f'Plugin \'{plugin_ref}\' is not enabled', str(response.content, 'utf8')
+            f"Plugin '{plugin_ref}' is not enabled", str(response.content, 'utf8')
         )
 
         # Active plugin
@@ -194,27 +194,27 @@ class LabelMixinTests(InvenTreeAPITestCase):
         options = self.options(
             self.do_url(parts, plugin_ref, label), expected_code=200
         ).json()
-        self.assertTrue("amount" in options["actions"]["POST"])
+        self.assertTrue('amount' in options['actions']['POST'])
 
         plg = registry.get_plugin(plugin_ref)
-        with mock.patch.object(plg, "print_label") as print_label:
+        with mock.patch.object(plg, 'print_label') as print_label:
             # wrong value type
             res = self.post(
                 self.do_url(parts, plugin_ref, label),
-                data={"amount": "-no-valid-int-"},
+                data={'amount': '-no-valid-int-'},
                 expected_code=400,
             ).json()
-            self.assertTrue("amount" in res)
+            self.assertTrue('amount' in res)
             print_label.assert_not_called()
 
             # correct value type
             self.post(
                 self.do_url(parts, plugin_ref, label),
-                data={"amount": 13},
+                data={'amount': 13},
                 expected_code=200,
             ).json()
             self.assertEqual(
-                print_label.call_args.kwargs["printing_options"], {"amount": 13}
+                print_label.call_args.kwargs['printing_options'], {'amount': 13}
             )
 
     def test_printing_endpoints(self):
diff --git a/InvenTree/plugin/base/locate/api.py b/InvenTree/plugin/base/locate/api.py
index 560d35e295..c3f921d010 100644
--- a/InvenTree/plugin/base/locate/api.py
+++ b/InvenTree/plugin/base/locate/api.py
@@ -37,7 +37,7 @@ class LocatePluginView(APIView):
         # StockLocation to identify
         location_pk = request.data.get('location', None)
 
-        data = {"success": "Identification plugin activated", "plugin": plugin}
+        data = {'success': 'Identification plugin activated', 'plugin': plugin}
 
         # StockItem takes priority
         if item_pk:
diff --git a/InvenTree/plugin/base/locate/mixins.py b/InvenTree/plugin/base/locate/mixins.py
index f04a8afd75..005e9a8fcd 100644
--- a/InvenTree/plugin/base/locate/mixins.py
+++ b/InvenTree/plugin/base/locate/mixins.py
@@ -26,7 +26,7 @@ class LocateMixin:
     class MixinMeta:
         """Meta for mixin."""
 
-        MIXIN_NAME = "Locate"
+        MIXIN_NAME = 'Locate'
 
     def __init__(self):
         """Register the mixin."""
@@ -46,7 +46,7 @@ class LocateMixin:
 
         Note: A custom implementation could always change this behaviour
         """
-        logger.info("LocateMixin: Attempting to locate StockItem pk=%s", item_pk)
+        logger.info('LocateMixin: Attempting to locate StockItem pk=%s', item_pk)
 
         from stock.models import StockItem
 
@@ -57,7 +57,7 @@ class LocateMixin:
                 self.locate_stock_location(item.location.pk)
 
         except StockItem.DoesNotExist:  # pragma: no cover
-            logger.warning("LocateMixin: StockItem pk={item_pk} not found")
+            logger.warning('LocateMixin: StockItem pk={item_pk} not found')
             pass
 
     def locate_stock_location(self, location_pk):
diff --git a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
index 7b0e7a4f7f..83c7b48f17 100644
--- a/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
+++ b/InvenTree/plugin/builtin/barcodes/inventree_barcode.py
@@ -21,11 +21,11 @@ from plugin.mixins import BarcodeMixin
 class InvenTreeInternalBarcodePlugin(BarcodeMixin, InvenTreePlugin):
     """Builtin BarcodePlugin for matching and generating internal barcodes."""
 
-    NAME = "InvenTreeBarcode"
-    TITLE = _("InvenTree Barcodes")
-    DESCRIPTION = _("Provides native support for barcodes")
-    VERSION = "2.0.0"
-    AUTHOR = _("InvenTree contributors")
+    NAME = 'InvenTreeBarcode'
+    TITLE = _('InvenTree Barcodes')
+    DESCRIPTION = _('Provides native support for barcodes')
+    VERSION = '2.0.0'
+    AUTHOR = _('InvenTree contributors')
 
     @staticmethod
     def get_supported_barcode_models():
diff --git a/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py b/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py
index eead5d6f36..388376f002 100644
--- a/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py
+++ b/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py
@@ -118,7 +118,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase):
         si = stock.models.StockItem.objects.get(pk=521)
 
         self.assertEqual(si.barcode_data, bc_data)
-        self.assertEqual(si.barcode_hash, "2f5dba5c83a360599ba7665b2a4131c6")
+        self.assertEqual(si.barcode_hash, '2f5dba5c83a360599ba7665b2a4131c6')
 
         # Now test that we cannot assign this barcode to something else
         response = self.assign(
diff --git a/InvenTree/plugin/builtin/integration/core_notifications.py b/InvenTree/plugin/builtin/integration/core_notifications.py
index b2e76f9bcd..e882562ba2 100644
--- a/InvenTree/plugin/builtin/integration/core_notifications.py
+++ b/InvenTree/plugin/builtin/integration/core_notifications.py
@@ -30,11 +30,11 @@ class InvenTreeCoreNotificationsPlugin(
 ):
     """Core notification methods for InvenTree."""
 
-    NAME = "InvenTreeCoreNotificationsPlugin"
-    TITLE = _("InvenTree Notifications")
+    NAME = 'InvenTreeCoreNotificationsPlugin'
+    TITLE = _('InvenTree Notifications')
     AUTHOR = _('InvenTree contributors')
     DESCRIPTION = _('Integrated outgoing notification methods')
-    VERSION = "1.0.0"
+    VERSION = '1.0.0'
 
     SETTINGS = {
         'ENABLE_NOTIFICATION_EMAILS': {
@@ -145,28 +145,28 @@ class InvenTreeCoreNotificationsPlugin(
                     'text': str(self.context['message']),
                     'blocks': [
                         {
-                            "type": "section",
-                            "text": {
-                                "type": "plain_text",
-                                "text": str(self.context['name']),
+                            'type': 'section',
+                            'text': {
+                                'type': 'plain_text',
+                                'text': str(self.context['name']),
                             },
                         },
                         {
-                            "type": "section",
-                            "text": {
-                                "type": "mrkdwn",
-                                "text": str(self.context['message']),
+                            'type': 'section',
+                            'text': {
+                                'type': 'mrkdwn',
+                                'text': str(self.context['message']),
                             },
-                            "accessory": {
-                                "type": "button",
-                                "text": {
-                                    "type": "plain_text",
-                                    "text": str(_("Open link")),
-                                    "emoji": True,
+                            'accessory': {
+                                'type': 'button',
+                                'text': {
+                                    'type': 'plain_text',
+                                    'text': str(_('Open link')),
+                                    'emoji': True,
                                 },
-                                "value": f'{self.category}_{self.obj.pk}',
-                                "url": self.context['link'],
-                                "action_id": "button-action",
+                                'value': f'{self.category}_{self.obj.pk}',
+                                'url': self.context['link'],
+                                'action_id': 'button-action',
                             },
                         },
                     ],
diff --git a/InvenTree/plugin/builtin/integration/currency_exchange.py b/InvenTree/plugin/builtin/integration/currency_exchange.py
index 383bc87566..7a57371ce4 100644
--- a/InvenTree/plugin/builtin/integration/currency_exchange.py
+++ b/InvenTree/plugin/builtin/integration/currency_exchange.py
@@ -16,12 +16,12 @@ class InvenTreeCurrencyExchange(APICallMixin, CurrencyExchangeMixin, InvenTreePl
     Fetches exchange rate information from frankfurter.app
     """
 
-    NAME = "InvenTreeCurrencyExchange"
-    SLUG = "inventreecurrencyexchange"
+    NAME = 'InvenTreeCurrencyExchange'
+    SLUG = 'inventreecurrencyexchange'
     AUTHOR = _('InvenTree contributors')
-    TITLE = _("InvenTree Currency Exchange")
-    DESCRIPTION = _("Default currency exchange integration")
-    VERSION = "1.0.0"
+    TITLE = _('InvenTree Currency Exchange')
+    DESCRIPTION = _('Default currency exchange integration')
+    VERSION = '1.0.0'
 
     def update_exchange_rates(self, base_currency: str, symbols: list[str]) -> dict:
         """Request exchange rate data from external API"""
@@ -37,7 +37,7 @@ class InvenTreeCurrencyExchange(APICallMixin, CurrencyExchangeMixin, InvenTreePl
 
             return rates
         logger.warning(
-            "Failed to update exchange rates from %s: Server returned status %s",
+            'Failed to update exchange rates from %s: Server returned status %s',
             self.api_url,
             response.status_code,
         )
diff --git a/InvenTree/plugin/builtin/labels/inventree_label.py b/InvenTree/plugin/builtin/labels/inventree_label.py
index 5ad9178b81..f4d18c0a73 100644
--- a/InvenTree/plugin/builtin/labels/inventree_label.py
+++ b/InvenTree/plugin/builtin/labels/inventree_label.py
@@ -16,11 +16,11 @@ class InvenTreeLabelPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlugin):
     which is made available for download.
     """
 
-    NAME = "InvenTreeLabel"
-    TITLE = _("InvenTree PDF label printer")
-    DESCRIPTION = _("Provides native support for printing PDF labels")
-    VERSION = "1.0.0"
-    AUTHOR = _("InvenTree contributors")
+    NAME = 'InvenTreeLabel'
+    TITLE = _('InvenTree PDF label printer')
+    DESCRIPTION = _('Provides native support for printing PDF labels')
+    VERSION = '1.0.0'
+    AUTHOR = _('InvenTree contributors')
 
     BLOCKING_PRINT = True
 
diff --git a/InvenTree/plugin/builtin/labels/label_sheet.py b/InvenTree/plugin/builtin/labels/label_sheet.py
index c81065e9b0..1d16aca1b9 100644
--- a/InvenTree/plugin/builtin/labels/label_sheet.py
+++ b/InvenTree/plugin/builtin/labels/label_sheet.py
@@ -56,11 +56,11 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
     and returns the resulting PDF file.
     """
 
-    NAME = "InvenTreeLabelSheet"
-    TITLE = _("InvenTree Label Sheet Printer")
-    DESCRIPTION = _("Arrays multiple labels onto a single sheet")
-    VERSION = "1.0.0"
-    AUTHOR = _("InvenTree contributors")
+    NAME = 'InvenTreeLabelSheet'
+    TITLE = _('InvenTree Label Sheet Printer')
+    DESCRIPTION = _('Arrays multiple labels onto a single sheet')
+    VERSION = '1.0.0'
+    AUTHOR = _('InvenTree contributors')
 
     BLOCKING_PRINT = True
 
@@ -92,7 +92,7 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
         n_cells = n_cols * n_rows
 
         if n_cells == 0:
-            raise ValidationError(_("Label is too large for page size"))
+            raise ValidationError(_('Label is too large for page size'))
 
         # Prepend the required number of skipped null labels
         items = [None] * skip + list(items)
@@ -101,16 +101,16 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
 
         # Data to pass through to each page
         document_data = {
-            "border": border,
-            "landscape": landscape,
-            "page_width": page_width,
-            "page_height": page_height,
-            "label_width": label.width,
-            "label_height": label.height,
-            "n_labels": n_labels,
-            "n_pages": math.ceil(n_labels / n_cells),
-            "n_cols": n_cols,
-            "n_rows": n_rows,
+            'border': border,
+            'landscape': landscape,
+            'page_width': page_width,
+            'page_height': page_height,
+            'label_width': label.width,
+            'label_height': label.height,
+            'n_labels': n_labels,
+            'n_pages': math.ceil(n_labels / n_cells),
+            'n_cols': n_cols,
+            'n_rows': n_rows,
         }
 
         pages = []
@@ -126,7 +126,7 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
             idx += n_cells
 
         if len(pages) == 0:
-            raise ValidationError(_("No labels were generated"))
+            raise ValidationError(_('No labels were generated'))
 
         # Render to a single HTML document
         html_data = self.wrap_pages(pages, **document_data)
@@ -191,16 +191,16 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
                         )
                         html += cell
                     except Exception as exc:
-                        logger.exception("Error rendering label: %s", str(exc))
+                        logger.exception('Error rendering label: %s', str(exc))
                         html += """
                         <div class='label-sheet-cell-error'></div>
                         """
 
-                html += "</td>"
+                html += '</td>'
 
-            html += "</tr>"
+            html += '</tr>'
 
-        html += "</table>"
+        html += '</table>'
 
         return html
 
@@ -241,7 +241,7 @@ class InvenTreeLabelSheetPlugin(LabelPrintingMixin, SettingsMixin, InvenTreePlug
             """
             )
 
-        cell_styles = "\n".join(cell_styles)
+        cell_styles = '\n'.join(cell_styles)
 
         return f"""
         <head>
diff --git a/InvenTree/plugin/builtin/suppliers/digikey.py b/InvenTree/plugin/builtin/suppliers/digikey.py
index bf8da46dd1..c441f9a9d9 100644
--- a/InvenTree/plugin/builtin/suppliers/digikey.py
+++ b/InvenTree/plugin/builtin/suppliers/digikey.py
@@ -12,19 +12,19 @@ from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
 class DigiKeyPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
     """Plugin to integrate the DigiKey API into Inventree."""
 
-    NAME = "DigiKeyPlugin"
-    TITLE = _("Supplier Integration - DigiKey")
-    DESCRIPTION = _("Provides support for scanning DigiKey barcodes")
-    VERSION = "1.0.0"
-    AUTHOR = _("InvenTree contributors")
+    NAME = 'DigiKeyPlugin'
+    TITLE = _('Supplier Integration - DigiKey')
+    DESCRIPTION = _('Provides support for scanning DigiKey barcodes')
+    VERSION = '1.0.0'
+    AUTHOR = _('InvenTree contributors')
 
-    DEFAULT_SUPPLIER_NAME = "DigiKey"
+    DEFAULT_SUPPLIER_NAME = 'DigiKey'
 
     SETTINGS = {
-        "SUPPLIER_ID": {
-            "name": _("Supplier"),
-            "description": _("The Supplier which acts as 'DigiKey'"),
-            "model": "company.company",
+        'SUPPLIER_ID': {
+            'name': _('Supplier'),
+            'description': _("The Supplier which acts as 'DigiKey'"),
+            'model': 'company.company',
         }
     }
 
diff --git a/InvenTree/plugin/builtin/suppliers/lcsc.py b/InvenTree/plugin/builtin/suppliers/lcsc.py
index f47e7ac54b..ec4d688820 100644
--- a/InvenTree/plugin/builtin/suppliers/lcsc.py
+++ b/InvenTree/plugin/builtin/suppliers/lcsc.py
@@ -14,29 +14,29 @@ from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
 class LCSCPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
     """Plugin to integrate the LCSC API into Inventree."""
 
-    NAME = "LCSCPlugin"
-    TITLE = _("Supplier Integration - LCSC")
-    DESCRIPTION = _("Provides support for scanning LCSC barcodes")
-    VERSION = "1.0.0"
-    AUTHOR = _("InvenTree contributors")
+    NAME = 'LCSCPlugin'
+    TITLE = _('Supplier Integration - LCSC')
+    DESCRIPTION = _('Provides support for scanning LCSC barcodes')
+    VERSION = '1.0.0'
+    AUTHOR = _('InvenTree contributors')
 
-    DEFAULT_SUPPLIER_NAME = "LCSC"
+    DEFAULT_SUPPLIER_NAME = 'LCSC'
     SETTINGS = {
-        "SUPPLIER_ID": {
-            "name": _("Supplier"),
-            "description": _("The Supplier which acts as 'LCSC'"),
-            "model": "company.company",
+        'SUPPLIER_ID': {
+            'name': _('Supplier'),
+            'description': _("The Supplier which acts as 'LCSC'"),
+            'model': 'company.company',
         }
     }
 
-    LCSC_BARCODE_REGEX = re.compile(r"^{((?:[^:,]+:[^:,]*,)*(?:[^:,]+:[^:,]*))}$")
+    LCSC_BARCODE_REGEX = re.compile(r'^{((?:[^:,]+:[^:,]*,)*(?:[^:,]+:[^:,]*))}$')
 
     # Custom field mapping for LCSC barcodes
     LCSC_FIELDS = {
-        "pm": SupplierBarcodeMixin.MANUFACTURER_PART_NUMBER,
-        "pc": SupplierBarcodeMixin.SUPPLIER_PART_NUMBER,
-        "qty": SupplierBarcodeMixin.QUANTITY,
-        "on": SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER,
+        'pm': SupplierBarcodeMixin.MANUFACTURER_PART_NUMBER,
+        'pc': SupplierBarcodeMixin.SUPPLIER_PART_NUMBER,
+        'qty': SupplierBarcodeMixin.QUANTITY,
+        'on': SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER,
     }
 
     def extract_barcode_fields(self, barcode_data: str) -> dict[str, str]:
@@ -53,7 +53,7 @@ class LCSCPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
             barcode_data, delimiter=',', header='{', trailer='}'
         )
 
-        fields = dict(pair.split(":") for pair in fields)
+        fields = dict(pair.split(':') for pair in fields)
 
         barcode_fields = {}
 
diff --git a/InvenTree/plugin/builtin/suppliers/mouser.py b/InvenTree/plugin/builtin/suppliers/mouser.py
index 6b75f85535..fda6421ac8 100644
--- a/InvenTree/plugin/builtin/suppliers/mouser.py
+++ b/InvenTree/plugin/builtin/suppliers/mouser.py
@@ -12,18 +12,18 @@ from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
 class MouserPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
     """Plugin to integrate the Mouser API into Inventree."""
 
-    NAME = "MouserPlugin"
-    TITLE = _("Supplier Integration - Mouser")
-    DESCRIPTION = _("Provides support for scanning Mouser barcodes")
-    VERSION = "1.0.0"
-    AUTHOR = _("InvenTree contributors")
+    NAME = 'MouserPlugin'
+    TITLE = _('Supplier Integration - Mouser')
+    DESCRIPTION = _('Provides support for scanning Mouser barcodes')
+    VERSION = '1.0.0'
+    AUTHOR = _('InvenTree contributors')
 
-    DEFAULT_SUPPLIER_NAME = "Mouser"
+    DEFAULT_SUPPLIER_NAME = 'Mouser'
     SETTINGS = {
-        "SUPPLIER_ID": {
-            "name": _("Supplier"),
-            "description": _("The Supplier which acts as 'Mouser'"),
-            "model": "company.company",
+        'SUPPLIER_ID': {
+            'name': _('Supplier'),
+            'description': _("The Supplier which acts as 'Mouser'"),
+            'model': 'company.company',
         }
     }
 
diff --git a/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py b/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py
index f6491eec6b..afd1dea435 100644
--- a/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py
+++ b/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py
@@ -12,35 +12,35 @@ from stock.models import StockItem, StockLocation
 class SupplierBarcodeTests(InvenTreeAPITestCase):
     """Tests barcode parsing for all suppliers."""
 
-    SCAN_URL = reverse("api-barcode-scan")
+    SCAN_URL = reverse('api-barcode-scan')
 
     @classmethod
     def setUpTestData(cls):
         """Create supplier parts for barcodes."""
         super().setUpTestData()
 
-        part = Part.objects.create(name="Test Part", description="Test Part")
+        part = Part.objects.create(name='Test Part', description='Test Part')
 
         manufacturer = Company.objects.create(
-            name="Test Manufacturer", is_manufacturer=True
+            name='Test Manufacturer', is_manufacturer=True
         )
 
         mpart1 = ManufacturerPart.objects.create(
-            part=part, manufacturer=manufacturer, MPN="MC34063ADR"
+            part=part, manufacturer=manufacturer, MPN='MC34063ADR'
         )
         mpart2 = ManufacturerPart.objects.create(
-            part=part, manufacturer=manufacturer, MPN="LDK320ADU33R"
+            part=part, manufacturer=manufacturer, MPN='LDK320ADU33R'
         )
 
-        supplier = Company.objects.create(name="Supplier", is_supplier=True)
-        mouser = Company.objects.create(name="Mouser Test", is_supplier=True)
+        supplier = Company.objects.create(name='Supplier', is_supplier=True)
+        mouser = Company.objects.create(name='Mouser Test', is_supplier=True)
 
         supplier_parts = [
-            SupplierPart(SKU="296-LM358BIDDFRCT-ND", part=part, supplier=supplier),
-            SupplierPart(SKU="1", part=part, manufacturer_part=mpart1, supplier=mouser),
-            SupplierPart(SKU="2", part=part, manufacturer_part=mpart2, supplier=mouser),
-            SupplierPart(SKU="C312270", part=part, supplier=supplier),
-            SupplierPart(SKU="WBP-302", part=part, supplier=supplier),
+            SupplierPart(SKU='296-LM358BIDDFRCT-ND', part=part, supplier=supplier),
+            SupplierPart(SKU='1', part=part, manufacturer_part=mpart1, supplier=mouser),
+            SupplierPart(SKU='2', part=part, manufacturer_part=mpart2, supplier=mouser),
+            SupplierPart(SKU='C312270', part=part, supplier=supplier),
+            SupplierPart(SKU='WBP-302', part=part, supplier=supplier),
         ]
 
         SupplierPart.objects.bulk_create(supplier_parts)
@@ -49,100 +49,100 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
         """Test digikey barcode"""
 
         result = self.post(
-            self.SCAN_URL, data={"barcode": DIGIKEY_BARCODE}, expected_code=200
+            self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE}, expected_code=200
         )
         self.assertEqual(result.data['plugin'], 'DigiKeyPlugin')
 
-        supplier_part_data = result.data.get("supplierpart")
+        supplier_part_data = result.data.get('supplierpart')
         self.assertIn('pk', supplier_part_data)
 
-        supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
-        self.assertEqual(supplier_part.SKU, "296-LM358BIDDFRCT-ND")
+        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
+        self.assertEqual(supplier_part.SKU, '296-LM358BIDDFRCT-ND')
 
     def test_digikey_2_barcode(self):
         """Test digikey barcode which uses 30P instead of P"""
         result = self.post(
-            self.SCAN_URL, data={"barcode": DIGIKEY_BARCODE_2}, expected_code=200
+            self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE_2}, expected_code=200
         )
         self.assertEqual(result.data['plugin'], 'DigiKeyPlugin')
 
-        supplier_part_data = result.data.get("supplierpart")
+        supplier_part_data = result.data.get('supplierpart')
         self.assertIn('pk', supplier_part_data)
 
-        supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
-        self.assertEqual(supplier_part.SKU, "296-LM358BIDDFRCT-ND")
+        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
+        self.assertEqual(supplier_part.SKU, '296-LM358BIDDFRCT-ND')
 
     def test_digikey_3_barcode(self):
         """Test digikey barcode which is invalid"""
-        self.post(self.SCAN_URL, data={"barcode": DIGIKEY_BARCODE_3}, expected_code=400)
+        self.post(self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE_3}, expected_code=400)
 
     def test_mouser_barcode(self):
         """Test mouser barcode with custom order number."""
 
         result = self.post(
-            self.SCAN_URL, data={"barcode": MOUSER_BARCODE}, expected_code=200
+            self.SCAN_URL, data={'barcode': MOUSER_BARCODE}, expected_code=200
         )
 
-        supplier_part_data = result.data.get("supplierpart")
+        supplier_part_data = result.data.get('supplierpart')
         self.assertIn('pk', supplier_part_data)
 
-        supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
+        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
         self.assertEqual(supplier_part.SKU, '1')
 
     def test_old_mouser_barcode(self):
         """Test old mouser barcode with messed up header."""
 
         result = self.post(
-            self.SCAN_URL, data={"barcode": MOUSER_BARCODE_OLD}, expected_code=200
+            self.SCAN_URL, data={'barcode': MOUSER_BARCODE_OLD}, expected_code=200
         )
 
-        supplier_part_data = result.data.get("supplierpart")
+        supplier_part_data = result.data.get('supplierpart')
         self.assertIn('pk', supplier_part_data)
-        supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
+        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
         self.assertEqual(supplier_part.SKU, '2')
 
     def test_lcsc_barcode(self):
         """Test LCSC barcode."""
 
         result = self.post(
-            self.SCAN_URL, data={"barcode": LCSC_BARCODE}, expected_code=200
+            self.SCAN_URL, data={'barcode': LCSC_BARCODE}, expected_code=200
         )
 
         self.assertEqual(result.data['plugin'], 'LCSCPlugin')
 
-        supplier_part_data = result.data.get("supplierpart")
+        supplier_part_data = result.data.get('supplierpart')
         self.assertIn('pk', supplier_part_data)
 
-        supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
+        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
         self.assertEqual(supplier_part.SKU, 'C312270')
 
     def test_tme_qrcode(self):
         """Test TME QR-Code."""
 
         result = self.post(
-            self.SCAN_URL, data={"barcode": TME_QRCODE}, expected_code=200
+            self.SCAN_URL, data={'barcode': TME_QRCODE}, expected_code=200
         )
 
         self.assertEqual(result.data['plugin'], 'TMEPlugin')
 
-        supplier_part_data = result.data.get("supplierpart")
+        supplier_part_data = result.data.get('supplierpart')
         self.assertIn('pk', supplier_part_data)
-        supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
+        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
         self.assertEqual(supplier_part.SKU, 'WBP-302')
 
     def test_tme_barcode2d(self):
         """Test TME DataMatrix-Code."""
 
         result = self.post(
-            self.SCAN_URL, data={"barcode": TME_DATAMATRIX_CODE}, expected_code=200
+            self.SCAN_URL, data={'barcode': TME_DATAMATRIX_CODE}, expected_code=200
         )
 
         self.assertEqual(result.data['plugin'], 'TMEPlugin')
 
-        supplier_part_data = result.data.get("supplierpart")
+        supplier_part_data = result.data.get('supplierpart')
         self.assertIn('pk', supplier_part_data)
 
-        supplier_part = SupplierPart.objects.get(pk=supplier_part_data["pk"])
+        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
         self.assertEqual(supplier_part.SKU, 'WBP-302')
 
 
@@ -153,27 +153,27 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
         """Create supplier part and purchase_order."""
         super().setUp()
 
-        part = Part.objects.create(name="Test Part", description="Test Part")
-        supplier = Company.objects.create(name="Supplier", is_supplier=True)
+        part = Part.objects.create(name='Test Part', description='Test Part')
+        supplier = Company.objects.create(name='Supplier', is_supplier=True)
         manufacturer = Company.objects.create(
-            name="Test Manufacturer", is_manufacturer=True
+            name='Test Manufacturer', is_manufacturer=True
         )
 
-        mouser = Company.objects.create(name="Mouser Test", is_supplier=True)
+        mouser = Company.objects.create(name='Mouser Test', is_supplier=True)
         mpart = ManufacturerPart.objects.create(
-            part=part, manufacturer=manufacturer, MPN="MC34063ADR"
+            part=part, manufacturer=manufacturer, MPN='MC34063ADR'
         )
 
         self.purchase_order1 = PurchaseOrder.objects.create(
-            supplier_reference="72991337", supplier=supplier
+            supplier_reference='72991337', supplier=supplier
         )
 
         supplier_parts1 = [
-            SupplierPart(SKU=f"1_{i}", part=part, supplier=supplier) for i in range(6)
+            SupplierPart(SKU=f'1_{i}', part=part, supplier=supplier) for i in range(6)
         ]
 
         supplier_parts1.insert(
-            2, SupplierPart(SKU="296-LM358BIDDFRCT-ND", part=part, supplier=supplier)
+            2, SupplierPart(SKU='296-LM358BIDDFRCT-ND', part=part, supplier=supplier)
         )
 
         for supplier_part in supplier_parts1:
@@ -181,17 +181,17 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
             self.purchase_order1.add_line_item(supplier_part, 8)
 
         self.purchase_order2 = PurchaseOrder.objects.create(
-            reference="P0-1337", supplier=mouser
+            reference='P0-1337', supplier=mouser
         )
 
         self.purchase_order2.place_order()
         supplier_parts2 = [
-            SupplierPart(SKU=f"2_{i}", part=part, supplier=mouser) for i in range(6)
+            SupplierPart(SKU=f'2_{i}', part=part, supplier=mouser) for i in range(6)
         ]
 
         supplier_parts2.insert(
             3,
-            SupplierPart(SKU="42", part=part, manufacturer_part=mpart, supplier=mouser),
+            SupplierPart(SKU='42', part=part, manufacturer_part=mpart, supplier=mouser),
         )
 
         for supplier_part in supplier_parts2:
@@ -201,185 +201,185 @@ class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
     def test_receive(self):
         """Test receiving an item from a barcode."""
 
-        url = reverse("api-barcode-po-receive")
+        url = reverse('api-barcode-po-receive')
 
-        result1 = self.post(url, data={"barcode": DIGIKEY_BARCODE}, expected_code=400)
+        result1 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400)
 
-        assert result1.data["error"].startswith("No matching purchase order")
+        assert result1.data['error'].startswith('No matching purchase order')
 
         self.purchase_order1.place_order()
 
-        result2 = self.post(url, data={"barcode": DIGIKEY_BARCODE}, expected_code=200)
-        self.assertIn("success", result2.data)
+        result2 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=200)
+        self.assertIn('success', result2.data)
 
-        result3 = self.post(url, data={"barcode": DIGIKEY_BARCODE}, expected_code=400)
-        self.assertEqual(result3.data['error'], "Item has already been received")
+        result3 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400)
+        self.assertEqual(result3.data['error'], 'Item has already been received')
 
         result4 = self.post(
-            url, data={"barcode": DIGIKEY_BARCODE[:-1]}, expected_code=400
+            url, data={'barcode': DIGIKEY_BARCODE[:-1]}, expected_code=400
         )
-        assert result4.data["error"].startswith(
-            "Failed to find pending line item for supplier part"
+        assert result4.data['error'].startswith(
+            'Failed to find pending line item for supplier part'
         )
 
         result5 = self.post(
-            reverse("api-barcode-scan"),
-            data={"barcode": DIGIKEY_BARCODE},
+            reverse('api-barcode-scan'),
+            data={'barcode': DIGIKEY_BARCODE},
             expected_code=200,
         )
-        stock_item = StockItem.objects.get(pk=result5.data["stockitem"]["pk"])
-        assert stock_item.supplier_part.SKU == "296-LM358BIDDFRCT-ND"
+        stock_item = StockItem.objects.get(pk=result5.data['stockitem']['pk'])
+        assert stock_item.supplier_part.SKU == '296-LM358BIDDFRCT-ND'
         assert stock_item.quantity == 10
         assert stock_item.location is None
 
     def test_receive_custom_order_number(self):
         """Test receiving an item from a barcode with a custom order number."""
 
-        url = reverse("api-barcode-po-receive")
-        result1 = self.post(url, data={"barcode": MOUSER_BARCODE})
-        assert "success" in result1.data
+        url = reverse('api-barcode-po-receive')
+        result1 = self.post(url, data={'barcode': MOUSER_BARCODE})
+        assert 'success' in result1.data
 
         result2 = self.post(
-            reverse("api-barcode-scan"), data={"barcode": MOUSER_BARCODE}
+            reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE}
         )
-        stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
-        assert stock_item.supplier_part.SKU == "42"
-        assert stock_item.supplier_part.manufacturer_part.MPN == "MC34063ADR"
+        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
+        assert stock_item.supplier_part.SKU == '42'
+        assert stock_item.supplier_part.manufacturer_part.MPN == 'MC34063ADR'
         assert stock_item.quantity == 3
         assert stock_item.location is None
 
     def test_receive_one_stock_location(self):
         """Test receiving an item when only one stock location exists"""
 
-        stock_location = StockLocation.objects.create(name="Test Location")
+        stock_location = StockLocation.objects.create(name='Test Location')
 
-        url = reverse("api-barcode-po-receive")
-        result1 = self.post(url, data={"barcode": MOUSER_BARCODE})
-        assert "success" in result1.data
+        url = reverse('api-barcode-po-receive')
+        result1 = self.post(url, data={'barcode': MOUSER_BARCODE})
+        assert 'success' in result1.data
 
         result2 = self.post(
-            reverse("api-barcode-scan"), data={"barcode": MOUSER_BARCODE}
+            reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE}
         )
-        stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
+        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
         assert stock_item.location == stock_location
 
     def test_receive_default_line_item_location(self):
         """Test receiving an item into the default line_item location"""
 
-        StockLocation.objects.create(name="Test Location 1")
-        stock_location2 = StockLocation.objects.create(name="Test Location 2")
+        StockLocation.objects.create(name='Test Location 1')
+        stock_location2 = StockLocation.objects.create(name='Test Location 2')
 
-        line_item = PurchaseOrderLineItem.objects.filter(part__SKU="42")[0]
+        line_item = PurchaseOrderLineItem.objects.filter(part__SKU='42')[0]
         line_item.destination = stock_location2
         line_item.save()
 
-        url = reverse("api-barcode-po-receive")
-        result1 = self.post(url, data={"barcode": MOUSER_BARCODE})
-        assert "success" in result1.data
+        url = reverse('api-barcode-po-receive')
+        result1 = self.post(url, data={'barcode': MOUSER_BARCODE})
+        assert 'success' in result1.data
 
         result2 = self.post(
-            reverse("api-barcode-scan"), data={"barcode": MOUSER_BARCODE}
+            reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE}
         )
-        stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
+        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
         assert stock_item.location == stock_location2
 
     def test_receive_default_part_location(self):
         """Test receiving an item into the default part location"""
 
-        StockLocation.objects.create(name="Test Location 1")
-        stock_location2 = StockLocation.objects.create(name="Test Location 2")
+        StockLocation.objects.create(name='Test Location 1')
+        stock_location2 = StockLocation.objects.create(name='Test Location 2')
 
         part = Part.objects.all()[0]
         part.default_location = stock_location2
         part.save()
 
-        url = reverse("api-barcode-po-receive")
-        result1 = self.post(url, data={"barcode": MOUSER_BARCODE})
-        assert "success" in result1.data
+        url = reverse('api-barcode-po-receive')
+        result1 = self.post(url, data={'barcode': MOUSER_BARCODE})
+        assert 'success' in result1.data
 
         result2 = self.post(
-            reverse("api-barcode-scan"), data={"barcode": MOUSER_BARCODE}
+            reverse('api-barcode-scan'), data={'barcode': MOUSER_BARCODE}
         )
-        stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
+        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
         assert stock_item.location == stock_location2
 
     def test_receive_specific_order_and_location(self):
         """Test receiving an item from a specific order into a specific location"""
 
-        StockLocation.objects.create(name="Test Location 1")
-        stock_location2 = StockLocation.objects.create(name="Test Location 2")
+        StockLocation.objects.create(name='Test Location 1')
+        stock_location2 = StockLocation.objects.create(name='Test Location 2')
 
-        url = reverse("api-barcode-po-receive")
-        barcode = MOUSER_BARCODE.replace("\x1dKP0-1337", "")
+        url = reverse('api-barcode-po-receive')
+        barcode = MOUSER_BARCODE.replace('\x1dKP0-1337', '')
         result1 = self.post(
             url,
             data={
-                "barcode": barcode,
-                "purchase_order": self.purchase_order2.pk,
-                "location": stock_location2.pk,
+                'barcode': barcode,
+                'purchase_order': self.purchase_order2.pk,
+                'location': stock_location2.pk,
             },
         )
-        assert "success" in result1.data
+        assert 'success' in result1.data
 
-        result2 = self.post(reverse("api-barcode-scan"), data={"barcode": barcode})
-        stock_item = StockItem.objects.get(pk=result2.data["stockitem"]["pk"])
+        result2 = self.post(reverse('api-barcode-scan'), data={'barcode': barcode})
+        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
         assert stock_item.location == stock_location2
 
     def test_receive_missing_quantity(self):
         """Test receiving an with missing quantity information"""
 
-        url = reverse("api-barcode-po-receive")
-        barcode = MOUSER_BARCODE.replace("\x1dQ3", "")
-        response = self.post(url, data={"barcode": barcode}, expected_code=200)
+        url = reverse('api-barcode-po-receive')
+        barcode = MOUSER_BARCODE.replace('\x1dQ3', '')
+        response = self.post(url, data={'barcode': barcode}, expected_code=200)
 
-        assert "lineitem" in response.data
-        assert "quantity" not in response.data["lineitem"]
+        assert 'lineitem' in response.data
+        assert 'quantity' not in response.data['lineitem']
 
 
 DIGIKEY_BARCODE = (
-    "[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1K72991337\x1d"
-    "10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337"
-    "\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000"
-    "00000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000"
+    '[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1K72991337\x1d'
+    '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337'
+    '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000'
+    '00000000000000000000000000000000000000000000000000000000000000000000000000'
+    '0000000000000000000000000000000000'
 )
 
 # Uses 30P instead of P
 DIGIKEY_BARCODE_2 = (
-    "[)>\x1e06\x1d30P296-LM358BIDDFRCT-ND\x1dK\x1d1K72991337\x1d"
-    "10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337"
-    "\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000"
-    "00000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000"
+    '[)>\x1e06\x1d30P296-LM358BIDDFRCT-ND\x1dK\x1d1K72991337\x1d'
+    '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337'
+    '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000'
+    '00000000000000000000000000000000000000000000000000000000000000000000000000'
+    '0000000000000000000000000000000000'
 )
 
 # Invalid code
 DIGIKEY_BARCODE_3 = (
-    "[)>\x1e06\x1dPnonsense\x1d30Pnonsense\x1d1Pnonsense\x1dK\x1d1K72991337\x1d"
-    "10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337"
-    "\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000"
-    "00000000000000000000000000000000000000000000000000000000000000000000000000"
-    "0000000000000000000000000000000000"
+    '[)>\x1e06\x1dPnonsense\x1d30Pnonsense\x1d1Pnonsense\x1dK\x1d1K72991337\x1d'
+    '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337'
+    '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000'
+    '00000000000000000000000000000000000000000000000000000000000000000000000000'
+    '0000000000000000000000000000000000'
 )
 
 MOUSER_BARCODE = (
-    "[)>\x1e06\x1dKP0-1337\x1d14K011\x1d1PMC34063ADR\x1dQ3\x1d11K073121337\x1d4"
-    "LMX\x1d1VTI\x1e\x04"
+    '[)>\x1e06\x1dKP0-1337\x1d14K011\x1d1PMC34063ADR\x1dQ3\x1d11K073121337\x1d4'
+    'LMX\x1d1VTI\x1e\x04'
 )
 
 MOUSER_BARCODE_OLD = (
-    ">[)>06\x1dK21421337\x1d14K033\x1d1PLDK320ADU33R\x1dQ32\x1d11K060931337\x1d"
-    "4LCN\x1d1VSTMicro"
+    '>[)>06\x1dK21421337\x1d14K033\x1d1PLDK320ADU33R\x1dQ32\x1d11K060931337\x1d'
+    '4LCN\x1d1VSTMicro'
 )
 
 LCSC_BARCODE = (
-    "{pbn:PICK2009291337,on:SO2009291337,pc:C312270,pm:ST-1-102-A01-T000-RS,qty"
-    ":2,mc:,cc:1,pdi:34421807}"
+    '{pbn:PICK2009291337,on:SO2009291337,pc:C312270,pm:ST-1-102-A01-T000-RS,qty'
+    ':2,mc:,cc:1,pdi:34421807}'
 )
 
 TME_QRCODE = (
-    "QTY:1 PN:WBP-302 PO:19361337/1 CPO:PO-2023-06-08-001337 MFR:WISHERENTERPRI"
-    "SE MPN:WBP-302 RoHS https://www.tme.eu/details/WBP-302"
+    'QTY:1 PN:WBP-302 PO:19361337/1 CPO:PO-2023-06-08-001337 MFR:WISHERENTERPRI'
+    'SE MPN:WBP-302 RoHS https://www.tme.eu/details/WBP-302'
 )
 
-TME_DATAMATRIX_CODE = "PWBP-302 1PMPNWBP-302 Q1 K19361337/1"
+TME_DATAMATRIX_CODE = 'PWBP-302 1PMPNWBP-302 Q1 K19361337/1'
diff --git a/InvenTree/plugin/builtin/suppliers/tme.py b/InvenTree/plugin/builtin/suppliers/tme.py
index 1be052274d..d0eb19503f 100644
--- a/InvenTree/plugin/builtin/suppliers/tme.py
+++ b/InvenTree/plugin/builtin/suppliers/tme.py
@@ -14,30 +14,30 @@ from plugin.mixins import SettingsMixin, SupplierBarcodeMixin
 class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
     """Plugin to integrate the TME API into Inventree."""
 
-    NAME = "TMEPlugin"
-    TITLE = _("Supplier Integration - TME")
-    DESCRIPTION = _("Provides support for scanning TME barcodes")
-    VERSION = "1.0.0"
-    AUTHOR = _("InvenTree contributors")
+    NAME = 'TMEPlugin'
+    TITLE = _('Supplier Integration - TME')
+    DESCRIPTION = _('Provides support for scanning TME barcodes')
+    VERSION = '1.0.0'
+    AUTHOR = _('InvenTree contributors')
 
-    DEFAULT_SUPPLIER_NAME = "TME"
+    DEFAULT_SUPPLIER_NAME = 'TME'
     SETTINGS = {
-        "SUPPLIER_ID": {
-            "name": _("Supplier"),
-            "description": _("The Supplier which acts as 'TME'"),
-            "model": "company.company",
+        'SUPPLIER_ID': {
+            'name': _('Supplier'),
+            'description': _("The Supplier which acts as 'TME'"),
+            'model': 'company.company',
         }
     }
 
-    TME_IS_QRCODE_REGEX = re.compile(r"([^\s:]+:[^\s:]+\s+)+(\S+(\s|$)+)+")
-    TME_IS_BARCODE2D_REGEX = re.compile(r"(([^\s]+)(\s+|$))+")
+    TME_IS_QRCODE_REGEX = re.compile(r'([^\s:]+:[^\s:]+\s+)+(\S+(\s|$)+)+')
+    TME_IS_BARCODE2D_REGEX = re.compile(r'(([^\s]+)(\s+|$))+')
 
     # Custom field mapping
     TME_QRCODE_FIELDS = {
-        "PN": SupplierBarcodeMixin.SUPPLIER_PART_NUMBER,
-        "PO": SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER,
-        "MPN": SupplierBarcodeMixin.MANUFACTURER_PART_NUMBER,
-        "QTY": SupplierBarcodeMixin.QUANTITY,
+        'PN': SupplierBarcodeMixin.SUPPLIER_PART_NUMBER,
+        'PO': SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER,
+        'MPN': SupplierBarcodeMixin.MANUFACTURER_PART_NUMBER,
+        'QTY': SupplierBarcodeMixin.QUANTITY,
     }
 
     def extract_barcode_fields(self, barcode_data: str) -> dict[str, str]:
@@ -47,9 +47,9 @@ class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
 
         if self.TME_IS_QRCODE_REGEX.fullmatch(barcode_data):
             # Custom QR Code format e.g. "QTY: 1 PN:12345"
-            for item in barcode_data.split(" "):
-                if ":" in item:
-                    key, value = item.split(":")
+            for item in barcode_data.split(' '):
+                if ':' in item:
+                    key, value = item.split(':')
                     if key in self.TME_QRCODE_FIELDS:
                         barcode_fields[self.TME_QRCODE_FIELDS[key]] = value
 
@@ -57,7 +57,7 @@ class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
 
         elif self.TME_IS_BARCODE2D_REGEX.fullmatch(barcode_data):
             # 2D Barcode format e.g. "PWBP-302 1PMPNWBP-302 Q1 K19361337/1"
-            for item in barcode_data.split(" "):
+            for item in barcode_data.split(' '):
                 for k, v in self.ecia_field_map().items():
                     if item.startswith(k):
                         barcode_fields[v] = item[len(k) :]
@@ -67,7 +67,7 @@ class TMEPlugin(SupplierBarcodeMixin, SettingsMixin, InvenTreePlugin):
         # Custom handling for order number
         if SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER in barcode_fields:
             order_number = barcode_fields[SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER]
-            order_number = order_number.split("/")[0]
+            order_number = order_number.split('/')[0]
             barcode_fields[SupplierBarcodeMixin.CUSTOMER_ORDER_NUMBER] = order_number
 
         return barcode_fields
diff --git a/InvenTree/plugin/helpers.py b/InvenTree/plugin/helpers.py
index 0dc3239512..211641514a 100644
--- a/InvenTree/plugin/helpers.py
+++ b/InvenTree/plugin/helpers.py
@@ -66,7 +66,7 @@ def log_error(error, reference: str = 'general'):
 def handle_error(error, do_raise: bool = True, do_log: bool = True, log_name: str = ''):
     """Handles an error and casts it as an IntegrationPluginError."""
     package_path = traceback.extract_tb(error.__traceback__)[-1].filename
-    install_path = sysconfig.get_paths()["purelib"]
+    install_path = sysconfig.get_paths()['purelib']
 
     try:
         package_name = pathlib.Path(package_path).relative_to(install_path).parts[0]
diff --git a/InvenTree/plugin/installer.py b/InvenTree/plugin/installer.py
index 113bcf014c..72b7ad4aa1 100644
--- a/InvenTree/plugin/installer.py
+++ b/InvenTree/plugin/installer.py
@@ -25,7 +25,7 @@ def pip_command(*args):
 
     command = [str(x) for x in command]
 
-    logger.info("Running pip command: %s", ' '.join(command))
+    logger.info('Running pip command: %s', ' '.join(command))
 
     return subprocess.check_output(
         command, cwd=settings.BASE_DIR.parent, stderr=subprocess.STDOUT
@@ -38,7 +38,7 @@ def check_package_path(packagename: str):
     - If installed, return the installation path
     - If not installed, return False
     """
-    logger.debug("check_package_path: %s", packagename)
+    logger.debug('check_package_path: %s', packagename)
 
     # Remove version information
     for c in '<>=! ':
@@ -47,7 +47,7 @@ def check_package_path(packagename: str):
     try:
         result = pip_command('show', packagename)
 
-        output = result.decode('utf-8').split("\n")
+        output = result.decode('utf-8').split('\n')
 
         for line in output:
             # Check if line matches pattern "Location: ..."
@@ -58,7 +58,7 @@ def check_package_path(packagename: str):
 
     except subprocess.CalledProcessError as error:
         output = error.output.decode('utf-8')
-        logger.exception("Plugin lookup failed: %s", str(output))
+        logger.exception('Plugin lookup failed: %s', str(output))
         return False
 
     # If we get here, the package is not installed
@@ -67,22 +67,22 @@ def check_package_path(packagename: str):
 
 def install_plugins_file():
     """Install plugins from the plugins file"""
-    logger.info("Installing plugins from plugins file")
+    logger.info('Installing plugins from plugins file')
 
     pf = settings.PLUGIN_FILE
 
     if not pf or not pf.exists():
-        logger.warning("Plugin file %s does not exist", str(pf))
+        logger.warning('Plugin file %s does not exist', str(pf))
         return
 
     try:
         pip_command('install', '-r', str(pf))
     except subprocess.CalledProcessError as error:
         output = error.output.decode('utf-8')
-        logger.exception("Plugin file installation failed: %s", str(output))
+        logger.exception('Plugin file installation failed: %s', str(output))
         return False
     except Exception as exc:
-        logger.exception("Plugin file installation failed: %s", exc)
+        logger.exception('Plugin file installation failed: %s', exc)
         return False
 
     # At this point, the plugins file has been installed
@@ -91,12 +91,12 @@ def install_plugins_file():
 
 def add_plugin_to_file(install_name):
     """Add a plugin to the plugins file"""
-    logger.info("Adding plugin to plugins file: %s", install_name)
+    logger.info('Adding plugin to plugins file: %s', install_name)
 
     pf = settings.PLUGIN_FILE
 
     if not pf or not pf.exists():
-        logger.warning("Plugin file %s does not exist", str(pf))
+        logger.warning('Plugin file %s does not exist', str(pf))
         return
 
     # First, read in existing plugin file
@@ -104,13 +104,13 @@ def add_plugin_to_file(install_name):
         with pf.open(mode='r') as f:
             lines = f.readlines()
     except Exception as exc:
-        logger.exception("Failed to read plugins file: %s", str(exc))
+        logger.exception('Failed to read plugins file: %s', str(exc))
         return
 
     # Check if plugin is already in file
     for line in lines:
         if line.strip() == install_name:
-            logger.debug("Plugin already exists in file")
+            logger.debug('Plugin already exists in file')
             return
 
     # Append plugin to file
@@ -125,7 +125,7 @@ def add_plugin_to_file(install_name):
                 if not line.endswith('\n'):
                     f.write('\n')
     except Exception as exc:
-        logger.exception("Failed to add plugin to plugins file: %s", str(exc))
+        logger.exception('Failed to add plugin to plugins file: %s', str(exc))
 
 
 def install_plugin(url=None, packagename=None, user=None):
@@ -136,17 +136,17 @@ def install_plugin(url=None, packagename=None, user=None):
     """
     if user and not user.is_staff:
         raise ValidationError(
-            _("Permission denied: only staff users can install plugins")
+            _('Permission denied: only staff users can install plugins')
         )
 
-    logger.debug("install_plugin: %s, %s", url, packagename)
+    logger.debug('install_plugin: %s, %s', url, packagename)
 
     # Check if we are running in a virtual environment
     # For now, just log a warning
     in_venv = sys.prefix != sys.base_prefix
 
     if not in_venv:
-        logger.warning("InvenTree is not running in a virtual environment")
+        logger.warning('InvenTree is not running in a virtual environment')
 
     # build up the command
     install_name = ['install', '-U']
@@ -185,23 +185,23 @@ def install_plugin(url=None, packagename=None, user=None):
     try:
         result = pip_command(*install_name)
 
-        ret['result'] = ret['success'] = _("Installed plugin successfully")
+        ret['result'] = ret['success'] = _('Installed plugin successfully')
         ret['output'] = str(result, 'utf-8')
 
         if packagename:
             if path := check_package_path(packagename):
                 # Override result information
-                ret['result'] = _(f"Installed plugin into {path}")
+                ret['result'] = _(f'Installed plugin into {path}')
 
     except subprocess.CalledProcessError as error:
         # If an error was thrown, we need to parse the output
 
         output = error.output.decode('utf-8')
-        logger.exception("Plugin installation failed: %s", str(output))
+        logger.exception('Plugin installation failed: %s', str(output))
 
-        errors = [_("Plugin installation failed")]
+        errors = [_('Plugin installation failed')]
 
-        for msg in output.split("\n"):
+        for msg in output.split('\n'):
             msg = msg.strip()
 
             if msg:
diff --git a/InvenTree/plugin/mock/simple.py b/InvenTree/plugin/mock/simple.py
index 8130aa7eed..3160fed4b8 100644
--- a/InvenTree/plugin/mock/simple.py
+++ b/InvenTree/plugin/mock/simple.py
@@ -7,4 +7,4 @@ class SimplePlugin(InvenTreePlugin):
     """A very simple plugin."""
 
     NAME = 'SimplePlugin'
-    SLUG = "simple"
+    SLUG = 'simple'
diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py
index fa3183e395..c8df8987c9 100644
--- a/InvenTree/plugin/models.py
+++ b/InvenTree/plugin/models.py
@@ -26,8 +26,8 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
     class Meta:
         """Meta for PluginConfig."""
 
-        verbose_name = _("Plugin Configuration")
-        verbose_name_plural = _("Plugin Configurations")
+        verbose_name = _('Plugin Configuration')
+        verbose_name_plural = _('Plugin Configurations')
 
     key = models.CharField(
         unique=True, max_length=255, verbose_name=_('Key'), help_text=_('Key of plugin')
@@ -114,7 +114,7 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
         """Customize pickling behavior."""
         state = super().__getstate__()
         state.pop(
-            "plugin", None
+            'plugin', None
         )  # plugin cannot be pickled in some circumstances when used with drf views, remove it (#5408)
         return state
 
diff --git a/InvenTree/plugin/plugin.py b/InvenTree/plugin/plugin.py
index 1cf5b28c9d..54fc1ff8d0 100644
--- a/InvenTree/plugin/plugin.py
+++ b/InvenTree/plugin/plugin.py
@@ -16,7 +16,7 @@ from django.utils.translation import gettext_lazy as _
 
 from plugin.helpers import get_git_log
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 class MetaBase:
diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py
index 5fa890bf34..2b3dc354b8 100644
--- a/InvenTree/plugin/registry.py
+++ b/InvenTree/plugin/registry.py
@@ -281,7 +281,7 @@ class PluginsRegistry:
         """
         # Do not reload when currently loading
         if self.is_loading:
-            logger.debug("Skipping reload - plugin registry is currently loading")
+            logger.debug('Skipping reload - plugin registry is currently loading')
             return
 
         if self.loading_lock.acquire(blocking=False):
@@ -347,7 +347,7 @@ class PluginsRegistry:
 
                     if not init_filename.exists():
                         try:
-                            init_filename.write_text("# InvenTree plugin directory\n")
+                            init_filename.write_text('# InvenTree plugin directory\n')
                         except Exception:  # pragma: no cover
                             logger.exception(
                                 "Could not create file '%s'", init_filename
@@ -415,7 +415,7 @@ class PluginsRegistry:
 
         # Log collected plugins
         logger.info('Collected %s plugins', len(collected_plugins))
-        logger.debug(", ".join([a.__module__ for a in collected_plugins]))
+        logger.debug(', '.join([a.__module__ for a in collected_plugins]))
 
         return collected_plugins
 
@@ -497,7 +497,7 @@ class PluginsRegistry:
                     raise error  # pragma: no cover
                 plg_db = None
             except IntegrityError as error:  # pragma: no cover
-                logger.exception("Error initializing plugin `%s`: %s", plg_name, error)
+                logger.exception('Error initializing plugin `%s`: %s', plg_name, error)
                 handle_error(error, log_name='init')
 
             # Append reference to plugin
@@ -533,7 +533,7 @@ class PluginsRegistry:
                     handle_error(
                         error, log_name='init'
                     )  # log error and raise it -> disable plugin
-                    logger.warning("Plugin `%s` could not be loaded", plg_name)
+                    logger.warning('Plugin `%s` could not be loaded', plg_name)
 
                 # Safe extra attributes
                 plg_i.is_package = getattr(plg_i, 'is_package', False)
@@ -694,25 +694,25 @@ class PluginsRegistry:
 
         try:
             old_hash = InvenTreeSetting.get_setting(
-                "_PLUGIN_REGISTRY_HASH", "", create=False, cache=False
+                '_PLUGIN_REGISTRY_HASH', '', create=False, cache=False
             )
         except Exception:
-            old_hash = ""
+            old_hash = ''
 
         if old_hash != self.registry_hash:
             try:
                 logger.debug(
-                    "Updating plugin registry hash: %s", str(self.registry_hash)
+                    'Updating plugin registry hash: %s', str(self.registry_hash)
                 )
                 InvenTreeSetting.set_setting(
-                    "_PLUGIN_REGISTRY_HASH", self.registry_hash, change_user=None
+                    '_PLUGIN_REGISTRY_HASH', self.registry_hash, change_user=None
                 )
             except (OperationalError, ProgrammingError):
                 # Exception if the database has not been migrated yet, or is not ready
                 pass
             except Exception as exc:
                 # Some other exception, we want to know about it
-                logger.exception("Failed to update plugin registry hash: %s", str(exc))
+                logger.exception('Failed to update plugin registry hash: %s', str(exc))
 
     def calculate_plugin_hash(self):
         """Calculate a 'hash' value for the current registry
@@ -767,7 +767,7 @@ class PluginsRegistry:
             # Skip check if database cannot be accessed
             return
 
-        logger.debug("Checking plugin registry hash")
+        logger.debug('Checking plugin registry hash')
 
         # If not already cached, calculate the hash
         if not self.registry_hash:
@@ -775,14 +775,14 @@ class PluginsRegistry:
 
         try:
             reg_hash = InvenTreeSetting.get_setting(
-                "_PLUGIN_REGISTRY_HASH", "", create=False, cache=False
+                '_PLUGIN_REGISTRY_HASH', '', create=False, cache=False
             )
         except Exception as exc:
-            logger.exception("Failed to retrieve plugin registry hash: %s", str(exc))
+            logger.exception('Failed to retrieve plugin registry hash: %s', str(exc))
             return
 
         if reg_hash and reg_hash != self.registry_hash:
-            logger.info("Plugin registry hash has changed - reloading")
+            logger.info('Plugin registry hash has changed - reloading')
             self.reload_plugins(full_reload=True, force_reload=True, collect=True)
 
     # endregion
diff --git a/InvenTree/plugin/samples/event/event_sample.py b/InvenTree/plugin/samples/event/event_sample.py
index 2ae21dcbb5..1b2bf59615 100644
--- a/InvenTree/plugin/samples/event/event_sample.py
+++ b/InvenTree/plugin/samples/event/event_sample.py
@@ -13,15 +13,15 @@ logger = logging.getLogger('inventree')
 class EventPluginSample(EventMixin, InvenTreePlugin):
     """A sample plugin which provides supports for triggered events."""
 
-    NAME = "EventPlugin"
-    SLUG = "sampleevent"
-    TITLE = "Triggered Events"
+    NAME = 'EventPlugin'
+    SLUG = 'sampleevent'
+    TITLE = 'Triggered Events'
 
     def process_event(self, event, *args, **kwargs):
         """Custom event processing."""
         print(f"Processing triggered event: '{event}'")
-        print("args:", str(args))
-        print("kwargs:", str(kwargs))
+        print('args:', str(args))
+        print('kwargs:', str(kwargs))
 
         # Issue warning that we can test for
         if settings.PLUGIN_TESTING:
diff --git a/InvenTree/plugin/samples/event/filtered_event_sample.py b/InvenTree/plugin/samples/event/filtered_event_sample.py
index 4267f3be1b..7380d0622a 100644
--- a/InvenTree/plugin/samples/event/filtered_event_sample.py
+++ b/InvenTree/plugin/samples/event/filtered_event_sample.py
@@ -13,19 +13,19 @@ logger = logging.getLogger('inventree')
 class FilteredEventPluginSample(EventMixin, InvenTreePlugin):
     """A sample plugin which provides supports for triggered events."""
 
-    NAME = "FilteredEventPlugin"
-    SLUG = "filteredsampleevent"
-    TITLE = "Triggered by test.event only"
+    NAME = 'FilteredEventPlugin'
+    SLUG = 'filteredsampleevent'
+    TITLE = 'Triggered by test.event only'
 
     def wants_process_event(self, event):
         """Return whether given event should be processed or not."""
-        return event == "test.event"
+        return event == 'test.event'
 
     def process_event(self, event, *args, **kwargs):
         """Custom event processing."""
         print(f"Processing triggered event: '{event}'")
-        print("args:", str(args))
-        print("kwargs:", str(kwargs))
+        print('args:', str(args))
+        print('kwargs:', str(kwargs))
 
         # Issue warning that we can test for
         if settings.PLUGIN_TESTING:
diff --git a/InvenTree/plugin/samples/event/test_event_sample.py b/InvenTree/plugin/samples/event/test_event_sample.py
index f1a8d103f0..0e5eb9b86d 100644
--- a/InvenTree/plugin/samples/event/test_event_sample.py
+++ b/InvenTree/plugin/samples/event/test_event_sample.py
@@ -27,7 +27,7 @@ class EventPluginSampleTests(TestCase):
         # Enable event testing
         settings.PLUGIN_TESTING_EVENTS = True
         # Check that an event is issued
-        with self.assertLogs(logger=logger, level="DEBUG") as cm:
+        with self.assertLogs(logger=logger, level='DEBUG') as cm:
             trigger_event('test.event')
         self.assertIn(
             'DEBUG:inventree:Event `test.event` triggered in sample plugin', cm[1]
diff --git a/InvenTree/plugin/samples/event/test_filtered_event_sample.py b/InvenTree/plugin/samples/event/test_filtered_event_sample.py
index fce2254785..1da5a3b499 100644
--- a/InvenTree/plugin/samples/event/test_filtered_event_sample.py
+++ b/InvenTree/plugin/samples/event/test_filtered_event_sample.py
@@ -25,7 +25,7 @@ class FilteredEventPluginSampleTests(TestCase):
         # Enable event testing
         settings.PLUGIN_TESTING_EVENTS = True
         # Check that an event is issued
-        with self.assertLogs(logger=logger, level="DEBUG") as cm:
+        with self.assertLogs(logger=logger, level='DEBUG') as cm:
             trigger_event('test.event')
         self.assertIn(
             'DEBUG:inventree:Event `test.event` triggered in sample plugin', cm[1]
@@ -46,7 +46,7 @@ class FilteredEventPluginSampleTests(TestCase):
         # Enable event testing
         settings.PLUGIN_TESTING_EVENTS = True
         # Check that an event is issued
-        with self.assertLogs(logger=logger, level="DEBUG") as cm:
+        with self.assertLogs(logger=logger, level='DEBUG') as cm:
             trigger_event('test.some.other.event')
         self.assertNotIn(
             'DEBUG:inventree:Event `test.some.other.event` triggered in sample plugin',
diff --git a/InvenTree/plugin/samples/integration/another_sample.py b/InvenTree/plugin/samples/integration/another_sample.py
index 9891584ab2..95cff15d2f 100644
--- a/InvenTree/plugin/samples/integration/another_sample.py
+++ b/InvenTree/plugin/samples/integration/another_sample.py
@@ -7,10 +7,10 @@ from plugin.mixins import UrlsMixin
 class NoIntegrationPlugin(InvenTreePlugin):
     """A basic plugin."""
 
-    NAME = "NoIntegrationPlugin"
+    NAME = 'NoIntegrationPlugin'
 
 
 class WrongIntegrationPlugin(UrlsMixin, InvenTreePlugin):
     """A basic wrong plugin with urls."""
 
-    NAME = "WrongIntegrationPlugin"
+    NAME = 'WrongIntegrationPlugin'
diff --git a/InvenTree/plugin/samples/integration/api_caller.py b/InvenTree/plugin/samples/integration/api_caller.py
index ca704a839e..ea78f175c5 100644
--- a/InvenTree/plugin/samples/integration/api_caller.py
+++ b/InvenTree/plugin/samples/integration/api_caller.py
@@ -7,7 +7,7 @@ from plugin.mixins import APICallMixin, SettingsMixin
 class SampleApiCallerPlugin(APICallMixin, SettingsMixin, InvenTreePlugin):
     """A small api call sample."""
 
-    NAME = "Sample API Caller"
+    NAME = 'Sample API Caller'
 
     SETTINGS = {
         'API_TOKEN': {'name': 'API Token', 'protected': True},
diff --git a/InvenTree/plugin/samples/integration/custom_panel_sample.py b/InvenTree/plugin/samples/integration/custom_panel_sample.py
index 695816ce47..9f6ccabb8c 100644
--- a/InvenTree/plugin/samples/integration/custom_panel_sample.py
+++ b/InvenTree/plugin/samples/integration/custom_panel_sample.py
@@ -9,11 +9,11 @@ from stock.views import StockLocationDetail
 class CustomPanelSample(PanelMixin, SettingsMixin, InvenTreePlugin):
     """A sample plugin which renders some custom panels."""
 
-    NAME = "CustomPanelExample"
-    SLUG = "samplepanel"
-    TITLE = "Custom Panel Example"
-    DESCRIPTION = "An example plugin demonstrating how custom panels can be added to the user interface"
-    VERSION = "0.1"
+    NAME = 'CustomPanelExample'
+    SLUG = 'samplepanel'
+    TITLE = 'Custom Panel Example'
+    DESCRIPTION = 'An example plugin demonstrating how custom panels can be added to the user interface'
+    VERSION = '0.1'
 
     SETTINGS = {
         'ENABLE_HELLO_WORLD': {
diff --git a/InvenTree/plugin/samples/integration/label_sample.py b/InvenTree/plugin/samples/integration/label_sample.py
index dbf5a65700..5ac88f532b 100644
--- a/InvenTree/plugin/samples/integration/label_sample.py
+++ b/InvenTree/plugin/samples/integration/label_sample.py
@@ -12,12 +12,12 @@ from plugin.mixins import LabelPrintingMixin
 class SampleLabelPrinter(LabelPrintingMixin, InvenTreePlugin):
     """Sample plugin which provides a 'fake' label printer endpoint."""
 
-    NAME = "Sample Label Printer"
-    SLUG = "samplelabelprinter"
-    TITLE = "Sample Label Printer"
-    DESCRIPTION = "A sample plugin which provides a (fake) label printer interface"
-    AUTHOR = "InvenTree contributors"
-    VERSION = "0.3.0"
+    NAME = 'Sample Label Printer'
+    SLUG = 'samplelabelprinter'
+    TITLE = 'Sample Label Printer'
+    DESCRIPTION = 'A sample plugin which provides a (fake) label printer interface'
+    AUTHOR = 'InvenTree contributors'
+    VERSION = '0.3.0'
 
     class PrintingOptionsSerializer(serializers.Serializer):
         """Serializer to return printing options."""
diff --git a/InvenTree/plugin/samples/integration/report_plugin_sample.py b/InvenTree/plugin/samples/integration/report_plugin_sample.py
index 7c8dbaa908..e14cadbfda 100644
--- a/InvenTree/plugin/samples/integration/report_plugin_sample.py
+++ b/InvenTree/plugin/samples/integration/report_plugin_sample.py
@@ -10,11 +10,11 @@ from report.models import PurchaseOrderReport
 class SampleReportPlugin(ReportMixin, InvenTreePlugin):
     """Sample plugin which provides extra context data to a report"""
 
-    NAME = "Sample Report Plugin"
-    SLUG = "samplereport"
-    TITLE = "Sample Report Plugin"
-    DESCRIPTION = "A sample plugin which provides extra context data to a report"
-    VERSION = "1.0"
+    NAME = 'Sample Report Plugin'
+    SLUG = 'samplereport'
+    TITLE = 'Sample Report Plugin'
+    DESCRIPTION = 'A sample plugin which provides extra context data to a report'
+    VERSION = '1.0'
 
     def some_custom_function(self):
         """Some custom function which is not required for the plugin to function"""
diff --git a/InvenTree/plugin/samples/integration/sample.py b/InvenTree/plugin/samples/integration/sample.py
index efdc342b52..894d5f51e3 100644
--- a/InvenTree/plugin/samples/integration/sample.py
+++ b/InvenTree/plugin/samples/integration/sample.py
@@ -24,11 +24,11 @@ class SampleIntegrationPlugin(
 ):
     """A full plugin example."""
 
-    NAME = "SampleIntegrationPlugin"
-    SLUG = "sample"
-    TITLE = "Sample Plugin"
+    NAME = 'SampleIntegrationPlugin'
+    SLUG = 'sample'
+    TITLE = 'Sample Plugin'
 
-    NAVIGATION_TAB_NAME = "Sample Nav"
+    NAVIGATION_TAB_NAME = 'Sample Nav'
     NAVIGATION_TAB_ICON = 'fas fa-plus'
 
     def view_test(self, request):
@@ -66,7 +66,7 @@ class SampleIntegrationPlugin(
             'default': 123,
         },
         'CHOICE_SETTING': {
-            'name': _("Choice Setting"),
+            'name': _('Choice Setting'),
             'description': _('A setting with multiple choices'),
             'choices': [('A', 'Anaconda'), ('B', 'Bat'), ('C', 'Cat'), ('D', 'Dog')],
             'default': 'A',
diff --git a/InvenTree/plugin/samples/integration/sample_currency_exchange.py b/InvenTree/plugin/samples/integration/sample_currency_exchange.py
index b26a438c58..a60a2eb11c 100644
--- a/InvenTree/plugin/samples/integration/sample_currency_exchange.py
+++ b/InvenTree/plugin/samples/integration/sample_currency_exchange.py
@@ -11,11 +11,11 @@ from plugin.mixins import CurrencyExchangeMixin
 class SampleCurrencyExchangePlugin(CurrencyExchangeMixin, InvenTreePlugin):
     """Dummy currency exchange plugin which provides fake exchange rates"""
 
-    NAME = "Sample Exchange"
-    DESCRIPTION = _("Sample currency exchange plugin")
-    SLUG = "samplecurrencyexchange"
-    VERSION = "0.1.0"
-    AUTHOR = _("InvenTree Contributors")
+    NAME = 'Sample Exchange'
+    DESCRIPTION = _('Sample currency exchange plugin')
+    SLUG = 'samplecurrencyexchange'
+    VERSION = '0.1.0'
+    AUTHOR = _('InvenTree Contributors')
 
     def update_exchange_rates(self, base_currency: str, symbols: list[str]) -> dict:
         """Return dummy data for some currencies"""
diff --git a/InvenTree/plugin/samples/integration/scheduled_task.py b/InvenTree/plugin/samples/integration/scheduled_task.py
index d2e4240877..3e8a8f574a 100644
--- a/InvenTree/plugin/samples/integration/scheduled_task.py
+++ b/InvenTree/plugin/samples/integration/scheduled_task.py
@@ -10,7 +10,7 @@ def print_hello():
 
     Contents do not matter - therefore no coverage.
     """
-    print("Hello")  # pragma: no cover
+    print('Hello')  # pragma: no cover
 
 
 def print_world():
@@ -18,16 +18,16 @@ def print_world():
 
     Contents do not matter - therefore no coverage.
     """
-    print("World")  # pragma: no cover
+    print('World')  # pragma: no cover
 
 
 class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin):
     """A sample plugin which provides support for scheduled tasks."""
 
-    NAME = "ScheduledTasksPlugin"
-    SLUG = "schedule"
-    TITLE = "Scheduled Tasks"
-    VERSION = "0.2.0"
+    NAME = 'ScheduledTasksPlugin'
+    SLUG = 'schedule'
+    TITLE = 'Scheduled Tasks'
+    VERSION = '0.2.0'
 
     SCHEDULED_TASKS = {
         'member': {'func': 'member_func', 'schedule': 'I', 'minutes': 30},
@@ -55,5 +55,5 @@ class ScheduledTaskPlugin(ScheduleMixin, SettingsMixin, InvenTreePlugin):
         """A simple member function to demonstrate functionality."""
         t_or_f = self.get_setting('T_OR_F')
 
-        print(f"Called member_func - value is {t_or_f}")
+        print(f'Called member_func - value is {t_or_f}')
         return t_or_f
diff --git a/InvenTree/plugin/samples/integration/simpleactionplugin.py b/InvenTree/plugin/samples/integration/simpleactionplugin.py
index 4e4b82f13d..bf892ba577 100644
--- a/InvenTree/plugin/samples/integration/simpleactionplugin.py
+++ b/InvenTree/plugin/samples/integration/simpleactionplugin.py
@@ -7,16 +7,16 @@ from plugin.mixins import ActionMixin
 class SimpleActionPlugin(ActionMixin, InvenTreePlugin):
     """An EXTREMELY simple action plugin which demonstrates the capability of the ActionMixin class."""
 
-    NAME = "SimpleActionPlugin"
-    ACTION_NAME = "simple"
+    NAME = 'SimpleActionPlugin'
+    ACTION_NAME = 'simple'
 
     def perform_action(self, user=None, data=None):
         """Sample method."""
-        print("Action plugin in action!")
+        print('Action plugin in action!')
 
     def get_info(self, user, data=None):
         """Sample method."""
-        return {"user": user.username, "hello": "world"}
+        return {'user': user.username, 'hello': 'world'}
 
     def get_result(self, user=None, data=None):
         """Sample method."""
diff --git a/InvenTree/plugin/samples/integration/test_sample.py b/InvenTree/plugin/samples/integration/test_sample.py
index 18e15f8721..0d7ac58893 100644
--- a/InvenTree/plugin/samples/integration/test_sample.py
+++ b/InvenTree/plugin/samples/integration/test_sample.py
@@ -46,7 +46,7 @@ class SampleIntegrationPluginTests(InvenTreeTestCase):
 
         # check settings
         self.assertEqual(plugin.check_settings(), (False, ['API_KEY']))
-        plugin.set_setting('API_KEY', "dsfiodsfjsfdjsf")
+        plugin.set_setting('API_KEY', 'dsfiodsfjsfdjsf')
         self.assertEqual(plugin.check_settings(), (True, []))
 
         # validator
diff --git a/InvenTree/plugin/samples/integration/test_scheduled_task.py b/InvenTree/plugin/samples/integration/test_scheduled_task.py
index 6a9518a917..c6513a7aa4 100644
--- a/InvenTree/plugin/samples/integration/test_scheduled_task.py
+++ b/InvenTree/plugin/samples/integration/test_scheduled_task.py
@@ -36,7 +36,7 @@ class ExampleScheduledTaskPluginTests(TestCase):
         # check that schedule was registers
         from django_q.models import Schedule
 
-        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.")
+        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith='plugin.')
         self.assertEqual(len(scheduled_plugin_tasks), 3)
 
         # test updating the schedule
@@ -48,7 +48,7 @@ class ExampleScheduledTaskPluginTests(TestCase):
 
         # Check that the schedule was updated
         hello_schedule = Schedule.objects.get(name='plugin.schedule.hello')
-        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.")
+        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith='plugin.')
         self.assertEqual(hello_schedule.minutes, 15)
         self.assertEqual(len(scheduled_plugin_tasks), 3)
 
@@ -56,12 +56,12 @@ class ExampleScheduledTaskPluginTests(TestCase):
         # this is to check the system also deals with disappearing tasks
         scheduled_plugin_tasks[1].delete()
         # there should be one less now
-        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.")
+        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith='plugin.')
         self.assertEqual(len(scheduled_plugin_tasks), 2)
 
         # test unregistering
         plg.unregister_tasks()
-        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith="plugin.")
+        scheduled_plugin_tasks = Schedule.objects.filter(name__istartswith='plugin.')
         self.assertEqual(len(scheduled_plugin_tasks), 0)
 
     def test_calling(self):
diff --git a/InvenTree/plugin/samples/integration/test_simpleactionplugin.py b/InvenTree/plugin/samples/integration/test_simpleactionplugin.py
index c6205be7e4..d1542c4c2d 100644
--- a/InvenTree/plugin/samples/integration/test_simpleactionplugin.py
+++ b/InvenTree/plugin/samples/integration/test_simpleactionplugin.py
@@ -15,21 +15,21 @@ class SimpleActionPluginTests(InvenTreeTestCase):
 
     def test_name(self):
         """Check plugn names."""
-        self.assertEqual(self.plugin.plugin_name(), "SimpleActionPlugin")
-        self.assertEqual(self.plugin.action_name(), "simple")
+        self.assertEqual(self.plugin.plugin_name(), 'SimpleActionPlugin')
+        self.assertEqual(self.plugin.action_name(), 'simple')
 
     def test_function(self):
         """Check if functions work."""
         # test functions
         response = self.client.post(
-            '/api/action/', data={'action': "simple", 'data': {'foo': "bar"}}
+            '/api/action/', data={'action': 'simple', 'data': {'foo': 'bar'}}
         )
         self.assertEqual(response.status_code, 200)
         self.assertJSONEqual(
             str(response.content, encoding='utf8'),
             {
-                "action": 'simple',
-                "result": True,
-                "info": {"user": self.username, "hello": "world"},
+                'action': 'simple',
+                'result': True,
+                'info': {'user': self.username, 'hello': 'world'},
             },
         )
diff --git a/InvenTree/plugin/samples/integration/transition.py b/InvenTree/plugin/samples/integration/transition.py
index bed1a91a5a..97166506bd 100644
--- a/InvenTree/plugin/samples/integration/transition.py
+++ b/InvenTree/plugin/samples/integration/transition.py
@@ -10,7 +10,7 @@ from plugin import InvenTreePlugin
 class SampleTransitionPlugin(InvenTreePlugin):
     """A sample plugin which shows how state transitions might be implemented."""
 
-    NAME = "SampleTransitionPlugin"
+    NAME = 'SampleTransitionPlugin'
 
     class ReturnChangeHandler(TransitionMethod):
         """Transition method for PurchaseOrder objects."""
@@ -32,7 +32,7 @@ class SampleTransitionPlugin(InvenTreePlugin):
                     'sampel_123_456',
                     targets=[instance.created_by],
                     context={
-                        'message': "Return order without responsible owner can not be completed!"
+                        'message': 'Return order without responsible owner can not be completed!'
                     },
                 )
                 return True  # True means nothing will happen
diff --git a/InvenTree/plugin/samples/integration/validation_sample.py b/InvenTree/plugin/samples/integration/validation_sample.py
index 67b84b636b..585ab59ed6 100644
--- a/InvenTree/plugin/samples/integration/validation_sample.py
+++ b/InvenTree/plugin/samples/integration/validation_sample.py
@@ -14,11 +14,11 @@ class CustomValidationMixin(SettingsMixin, ValidationMixin, InvenTreePlugin):
     Simple of examples of custom validator code.
     """
 
-    NAME = "CustomValidator"
-    SLUG = "validator"
-    TITLE = "Custom Validator Plugin"
-    DESCRIPTION = "A sample plugin for demonstrating custom validation functionality"
-    VERSION = "0.3.0"
+    NAME = 'CustomValidator'
+    SLUG = 'validator'
+    TITLE = 'Custom Validator Plugin'
+    DESCRIPTION = 'A sample plugin for demonstrating custom validation functionality'
+    VERSION = '0.3.0'
 
     SETTINGS = {
         'ILLEGAL_PART_CHARS': {
@@ -60,7 +60,7 @@ class CustomValidationMixin(SettingsMixin, ValidationMixin, InvenTreePlugin):
         These examples are silly, but serve to demonstrate how the feature could be used
         """
         if len(part.description) < len(name):
-            raise ValidationError("Part description cannot be shorter than the name")
+            raise ValidationError('Part description cannot be shorter than the name')
 
         illegal_chars = self.get_setting('ILLEGAL_PART_CHARS')
 
@@ -84,7 +84,7 @@ class CustomValidationMixin(SettingsMixin, ValidationMixin, InvenTreePlugin):
         if parameter.template.name.lower() in ['length', 'width']:
             d = int(data)
             if d >= 100:
-                raise ValidationError("Value must be less than 100")
+                raise ValidationError('Value must be less than 100')
 
     def validate_serial_number(self, serial: str, part):
         """Validate serial number for a given StockItem
@@ -93,13 +93,13 @@ class CustomValidationMixin(SettingsMixin, ValidationMixin, InvenTreePlugin):
         """
         if self.get_setting('SERIAL_MUST_BE_PALINDROME'):
             if serial != serial[::-1]:
-                raise ValidationError("Serial must be a palindrome")
+                raise ValidationError('Serial must be a palindrome')
 
         if self.get_setting('SERIAL_MUST_MATCH_PART'):
             # Serial must start with the same letter as the linked part, for some reason
             if serial[0] != part.name[0]:
                 raise ValidationError(
-                    "Serial number must start with same letter as part"
+                    'Serial number must start with same letter as part'
                 )
 
     def validate_batch_code(self, batch_code: str, item):
@@ -116,4 +116,4 @@ class CustomValidationMixin(SettingsMixin, ValidationMixin, InvenTreePlugin):
     def generate_batch_code(self):
         """Generate a new batch code."""
         now = datetime.now()
-        return f"BATCH-{now.year}:{now.month}:{now.day}"
+        return f'BATCH-{now.year}:{now.month}:{now.day}'
diff --git a/InvenTree/plugin/samples/integration/version.py b/InvenTree/plugin/samples/integration/version.py
index 78f9ccec73..eda28a5431 100644
--- a/InvenTree/plugin/samples/integration/version.py
+++ b/InvenTree/plugin/samples/integration/version.py
@@ -6,8 +6,8 @@ from plugin import InvenTreePlugin
 class VersionPlugin(InvenTreePlugin):
     """A small version sample."""
 
-    SLUG = "sampleversion"
-    NAME = "Sample Version Plugin"
-    DESCRIPTION = "A simple plugin which shows how to use the version limits"
+    SLUG = 'sampleversion'
+    NAME = 'Sample Version Plugin'
+    DESCRIPTION = 'A simple plugin which shows how to use the version limits'
     MIN_VERSION = '0.1.0'
     MAX_VERSION = '1.0.0'
diff --git a/InvenTree/plugin/samples/locate/locate_sample.py b/InvenTree/plugin/samples/locate/locate_sample.py
index 2a53bf79f6..50f3263f98 100644
--- a/InvenTree/plugin/samples/locate/locate_sample.py
+++ b/InvenTree/plugin/samples/locate/locate_sample.py
@@ -17,11 +17,11 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin):
     This plugin class simply prints location information to the logger.
     """
 
-    NAME = "SampleLocatePlugin"
-    SLUG = "samplelocate"
-    TITLE = "Sample plugin for locating items"
+    NAME = 'SampleLocatePlugin'
+    SLUG = 'samplelocate'
+    TITLE = 'Sample plugin for locating items'
 
-    VERSION = "0.2"
+    VERSION = '0.2'
 
     def locate_stock_item(self, item_pk):
         """Locate a StockItem.
@@ -31,17 +31,17 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin):
         """
         from stock.models import StockItem
 
-        logger.info("SampleLocatePlugin attempting to locate item ID %s", item_pk)
+        logger.info('SampleLocatePlugin attempting to locate item ID %s', item_pk)
 
         try:
             item = StockItem.objects.get(pk=item_pk)
-            logger.info("StockItem %s located!", item_pk)
+            logger.info('StockItem %s located!', item_pk)
 
             # Tag metadata
             item.set_metadata('located', True)
 
         except (ValueError, StockItem.DoesNotExist):  # pragma: no cover
-            logger.exception("StockItem ID %s does not exist!", item_pk)
+            logger.exception('StockItem ID %s does not exist!', item_pk)
 
     def locate_stock_location(self, location_pk):
         """Locate a StockLocation.
@@ -52,7 +52,7 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin):
         from stock.models import StockLocation
 
         logger.info(
-            "SampleLocatePlugin attempting to locate location ID %s", location_pk
+            'SampleLocatePlugin attempting to locate location ID %s', location_pk
         )
 
         try:
@@ -63,4 +63,4 @@ class SampleLocatePlugin(LocateMixin, InvenTreePlugin):
             location.set_metadata('located', True)
 
         except (ValueError, StockLocation.DoesNotExist):  # pragma: no cover
-            logger.exception("Location ID %s does not exist!", location_pk)
+            logger.exception('Location ID %s does not exist!', location_pk)
diff --git a/InvenTree/plugin/serializers.py b/InvenTree/plugin/serializers.py
index 57d17e6dd0..9d123ee952 100644
--- a/InvenTree/plugin/serializers.py
+++ b/InvenTree/plugin/serializers.py
@@ -135,24 +135,24 @@ class PluginReloadSerializer(serializers.Serializer):
     full_reload = serializers.BooleanField(
         required=False,
         default=False,
-        label=_("Full reload"),
-        help_text=_("Perform a full reload of the plugin registry"),
+        label=_('Full reload'),
+        help_text=_('Perform a full reload of the plugin registry'),
     )
 
     force_reload = serializers.BooleanField(
         required=False,
         default=False,
-        label=_("Force reload"),
+        label=_('Force reload'),
         help_text=_(
-            "Force a reload of the plugin registry, even if it is already loaded"
+            'Force a reload of the plugin registry, even if it is already loaded'
         ),
     )
 
     collect_plugins = serializers.BooleanField(
         required=False,
         default=False,
-        label=_("Collect plugins"),
-        help_text=_("Collect plugins and add them to the registry"),
+        label=_('Collect plugins'),
+        help_text=_('Collect plugins and add them to the registry'),
     )
 
     def save(self):
diff --git a/InvenTree/plugin/test_plugin.py b/InvenTree/plugin/test_plugin.py
index 9dc6d7f63f..0612cb70bb 100644
--- a/InvenTree/plugin/test_plugin.py
+++ b/InvenTree/plugin/test_plugin.py
@@ -98,7 +98,7 @@ class InvenTreePluginTests(TestCase):
             NAME = 'Aplugin'
             SLUG = 'a'
             TITLE = 'a title'
-            PUBLISH_DATE = "1111-11-11"
+            PUBLISH_DATE = '1111-11-11'
             AUTHOR = 'AA BB'
             DESCRIPTION = 'A description'
             VERSION = '1.2.3a'
diff --git a/InvenTree/report/api.py b/InvenTree/report/api.py
index 83e983fe96..8a5a6cbd10 100644
--- a/InvenTree/report/api.py
+++ b/InvenTree/report/api.py
@@ -73,7 +73,7 @@ class ReportFilterMixin:
         """Return a list of database objects from query parameters"""
         if not self.ITEM_MODEL:
             raise NotImplementedError(
-                f"ITEM_MODEL attribute not defined for {__class__}"
+                f'ITEM_MODEL attribute not defined for {__class__}'
             )
 
         ids = []
@@ -184,7 +184,7 @@ class ReportPrintMixin:
         )
 
         # Start with a default report name
-        report_name = "report.pdf"
+        report_name = 'report.pdf'
 
         try:
             # Merge one or more PDF files into a single download
@@ -223,7 +223,7 @@ class ReportPrintMixin:
             if debug_mode:
                 """Concatenate all rendered templates into a single HTML string, and return the string as a HTML response."""
 
-                html = "\n".join(outputs)
+                html = '\n'.join(outputs)
 
                 return HttpResponse(html)
             else:
@@ -320,7 +320,7 @@ class StockItemTestReportPrint(StockItemTestReportMixin, ReportPrintMixin, Retri
             # Construct a PDF file object
             try:
                 pdf = report.get_document().write_pdf()
-                pdf_content = ContentFile(pdf, "test_report.pdf")
+                pdf_content = ContentFile(pdf, 'test_report.pdf')
             except TemplateDoesNotExist:
                 return
 
@@ -328,7 +328,7 @@ class StockItemTestReportPrint(StockItemTestReportMixin, ReportPrintMixin, Retri
                 attachment=pdf_content,
                 stock_item=item,
                 user=request.user,
-                comment=_("Test report"),
+                comment=_('Test report'),
             )
 
 
diff --git a/InvenTree/report/apps.py b/InvenTree/report/apps.py
index 4362f5f4f4..19a7e9e284 100644
--- a/InvenTree/report/apps.py
+++ b/InvenTree/report/apps.py
@@ -11,7 +11,7 @@ from django.conf import settings
 from django.core.exceptions import AppRegistryNotReady
 from django.db.utils import IntegrityError, OperationalError, ProgrammingError
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 class ReportConfig(AppConfig):
diff --git a/InvenTree/report/helpers.py b/InvenTree/report/helpers.py
index 2088913db5..d3594300e3 100644
--- a/InvenTree/report/helpers.py
+++ b/InvenTree/report/helpers.py
@@ -46,7 +46,7 @@ def report_page_size_default():
     try:
         page_size = InvenTreeSetting.get_setting('REPORT_DEFAULT_PAGE_SIZE', 'A4')
     except Exception as exc:
-        logger.exception("Error getting default page size: %s", str(exc))
+        logger.exception('Error getting default page size: %s', str(exc))
         page_size = 'A4'
 
     return page_size
@@ -70,4 +70,4 @@ def encode_image_base64(image, format: str = 'PNG'):
 
     img_str = base64.b64encode(buffered.getvalue())
 
-    return f"data:image/{fmt};charset=utf-8;base64," + img_str.decode()
+    return f'data:image/{fmt};charset=utf-8;base64,' + img_str.decode()
diff --git a/InvenTree/report/models.py b/InvenTree/report/models.py
index f301ece04d..e64ae4abda 100644
--- a/InvenTree/report/models.py
+++ b/InvenTree/report/models.py
@@ -28,12 +28,12 @@ from plugin.registry import registry
 try:
     from django_weasyprint import WeasyTemplateResponseMixin
 except OSError as err:  # pragma: no cover
-    print(f"OSError: {err}")
-    print("You may require some further system packages to be installed.")
+    print(f'OSError: {err}')
+    print('You may require some further system packages to be installed.')
     sys.exit(1)
 
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 def rename_template(instance, filename):
@@ -118,7 +118,7 @@ class ReportBase(models.Model):
 
     def __str__(self):
         """Format a string representation of a report instance"""
-        return f"{self.name} - {self.description}"
+        return f'{self.name} - {self.description}'
 
     @classmethod
     def getSubdir(cls):
@@ -176,20 +176,20 @@ class ReportBase(models.Model):
     template = models.FileField(
         upload_to=rename_template,
         verbose_name=_('Template'),
-        help_text=_("Report template file"),
+        help_text=_('Report template file'),
         validators=[FileExtensionValidator(allowed_extensions=['html', 'htm'])],
     )
 
     description = models.CharField(
         max_length=250,
         verbose_name=_('Description'),
-        help_text=_("Report template description"),
+        help_text=_('Report template description'),
     )
 
     revision = models.PositiveIntegerField(
         default=1,
-        verbose_name=_("Revision"),
-        help_text=_("Report revision number (auto-increments)"),
+        verbose_name=_('Revision'),
+        help_text=_('Report revision number (auto-increments)'),
         editable=False,
     )
 
@@ -295,7 +295,7 @@ class ReportTemplateBase(MetadataMixin, ReportBase):
         wp = WeasyprintReportMixin(
             request,
             self.template_name,
-            base_url=request.build_absolute_uri("/"),
+            base_url=request.build_absolute_uri('/'),
             presentational_hints=True,
             filename=self.generate_filename(request),
             **kwargs,
@@ -304,7 +304,7 @@ class ReportTemplateBase(MetadataMixin, ReportBase):
         return wp.render_to_response(self.context(request), **kwargs)
 
     filename_pattern = models.CharField(
-        default="report.pdf",
+        default='report.pdf',
         verbose_name=_('Filename Pattern'),
         help_text=_('Pattern for generating report filenames'),
         max_length=100,
@@ -335,7 +335,7 @@ class TestReport(ReportTemplateBase):
         max_length=250,
         verbose_name=_('Filters'),
         help_text=_(
-            "StockItem query filters (comma-separated list of key=value pairs)"
+            'StockItem query filters (comma-separated list of key=value pairs)'
         ),
         validators=[validate_stock_item_report_filters],
     )
@@ -612,7 +612,7 @@ class ReportSnippet(models.Model):
     description = models.CharField(
         max_length=250,
         verbose_name=_('Description'),
-        help_text=_("Snippet file description"),
+        help_text=_('Snippet file description'),
     )
 
 
@@ -650,14 +650,14 @@ class ReportAsset(models.Model):
     asset = models.FileField(
         upload_to=rename_asset,
         verbose_name=_('Asset'),
-        help_text=_("Report asset file"),
+        help_text=_('Report asset file'),
     )
 
     # Asset description (user facing string, not used internally)
     description = models.CharField(
         max_length=250,
         verbose_name=_('Description'),
-        help_text=_("Asset file description"),
+        help_text=_('Asset file description'),
     )
 
 
@@ -679,7 +679,7 @@ class StockLocationReport(ReportTemplateBase):
         max_length=250,
         verbose_name=_('Filters'),
         help_text=_(
-            "stock location query filters (comma-separated list of key=value pairs)"
+            'stock location query filters (comma-separated list of key=value pairs)'
         ),
         validators=[validate_stock_location_report_filters],
     )
diff --git a/InvenTree/report/templatetags/barcode.py b/InvenTree/report/templatetags/barcode.py
index 268f00e504..451136eb0f 100644
--- a/InvenTree/report/templatetags/barcode.py
+++ b/InvenTree/report/templatetags/barcode.py
@@ -35,7 +35,7 @@ def qrcode(data, **kwargs):
 
     """
     # Construct "default" values
-    params = {"box_size": 20, "border": 1, "version": 1}
+    params = {'box_size': 20, 'border': 1, 'version': 1}
 
     fill_color = kwargs.pop('fill_color', 'black')
     back_color = kwargs.pop('back_color', 'white')
diff --git a/InvenTree/report/templatetags/report.py b/InvenTree/report/templatetags/report.py
index de1cb0544d..c2a096136f 100644
--- a/InvenTree/report/templatetags/report.py
+++ b/InvenTree/report/templatetags/report.py
@@ -63,7 +63,7 @@ def getkey(container: dict, key):
         key: The 'key' to be found within the dict
     """
     if type(container) is not dict:
-        logger.warning("getkey() called with non-dict object")
+        logger.warning('getkey() called with non-dict object')
         return None
 
     if key in container:
@@ -92,11 +92,11 @@ def asset(filename):
     full_path = settings.MEDIA_ROOT.joinpath('report', 'assets', filename).resolve()
 
     if not full_path.exists() or not full_path.is_file():
-        raise FileNotFoundError(_("Asset file does not exist") + f": '{filename}'")
+        raise FileNotFoundError(_('Asset file does not exist') + f": '{filename}'")
 
     if debug_mode:
         return os.path.join(settings.MEDIA_URL, 'report', 'assets', filename)
-    return f"file://{full_path}"
+    return f'file://{full_path}'
 
 
 @register.simple_tag()
@@ -147,7 +147,7 @@ def uploaded_image(
         exists = False
 
     if not exists and not replace_missing:
-        raise FileNotFoundError(_("Image file not found") + f": '{filename}'")
+        raise FileNotFoundError(_('Image file not found') + f": '{filename}'")
 
     if debug_mode:
         # In debug mode, return a web path (rather than an encoded image blob)
@@ -212,14 +212,14 @@ def encode_svg_image(filename):
             exists = False
 
     if not exists:
-        raise FileNotFoundError(_("Image file not found") + f": '{filename}'")
+        raise FileNotFoundError(_('Image file not found') + f": '{filename}'")
 
     # Read the file data
     with open(full_path, 'rb') as f:
         data = f.read()
 
     # Return the base64-encoded data
-    return "data:image/svg+xml;charset=utf-8;base64," + base64.b64encode(data).decode(
+    return 'data:image/svg+xml;charset=utf-8;base64,' + base64.b64encode(data).decode(
         'utf-8'
     )
 
@@ -235,7 +235,7 @@ def part_image(part: Part, preview=False, thumbnail=False, **kwargs):
         TypeError if provided part is not a Part instance
     """
     if type(part) is not Part:
-        raise TypeError(_("part_image tag requires a Part instance"))
+        raise TypeError(_('part_image tag requires a Part instance'))
 
     if preview:
         img = part.image.preview.name
@@ -274,7 +274,7 @@ def company_image(company, preview=False, thumbnail=False, **kwargs):
         TypeError if provided company is not a Company instance
     """
     if type(company) is not Company:
-        raise TypeError(_("company_image tag requires a Company instance"))
+        raise TypeError(_('company_image tag requires a Company instance'))
 
     if preview:
         img = company.image.preview.name
diff --git a/InvenTree/report/tests.py b/InvenTree/report/tests.py
index aa6c4fcb81..96f64a22a2 100644
--- a/InvenTree/report/tests.py
+++ b/InvenTree/report/tests.py
@@ -56,14 +56,14 @@ class ReportTagTest(TestCase):
             self.debug_mode(b)
 
             with self.assertRaises(FileNotFoundError):
-                report_tags.asset("bad_file.txt")
+                report_tags.asset('bad_file.txt')
 
         # Create an asset file
         asset_dir = settings.MEDIA_ROOT.joinpath('report', 'assets')
         asset_dir.mkdir(parents=True, exist_ok=True)
         asset_path = asset_dir.joinpath('test.txt')
 
-        asset_path.write_text("dummy data")
+        asset_path.write_text('dummy data')
 
         self.debug_mode(True)
         asset = report_tags.asset('test.txt')
@@ -101,7 +101,7 @@ class ReportTagTest(TestCase):
         img_file = img_path.joinpath('test.jpg')
 
         img_path.mkdir(parents=True, exist_ok=True)
-        img_file.write_text("dummy data")
+        img_file.write_text('dummy data')
 
         # Test in debug mode. Returns blank image as dummy file is not a valid image
         self.debug_mode(True)
@@ -158,7 +158,7 @@ class BarcodeTagTest(TestCase):
 
     def test_barcode(self):
         """Test the barcode generation tag"""
-        barcode = barcode_tags.barcode("12345")
+        barcode = barcode_tags.barcode('12345')
 
         self.assertTrue(isinstance(barcode, str))
         self.assertTrue(barcode.startswith('data:image/png;'))
@@ -171,14 +171,14 @@ class BarcodeTagTest(TestCase):
     def test_qrcode(self):
         """Test the qrcode generation tag"""
         # Test with default settings
-        qrcode = barcode_tags.qrcode("hello world")
+        qrcode = barcode_tags.qrcode('hello world')
         self.assertTrue(isinstance(qrcode, str))
         self.assertTrue(qrcode.startswith('data:image/png;'))
         self.assertEqual(len(qrcode), 700)
 
         # Generate a much larger qrcode
         qrcode = barcode_tags.qrcode(
-            "hello_world", version=2, box_size=50, format='BMP'
+            'hello_world', version=2, box_size=50, format='BMP'
         )
         self.assertTrue(isinstance(qrcode, str))
         self.assertTrue(qrcode.startswith('data:image/bmp;'))
diff --git a/InvenTree/script/translation_stats.py b/InvenTree/script/translation_stats.py
index 2e197ae6aa..f7a848f042 100644
--- a/InvenTree/script/translation_stats.py
+++ b/InvenTree/script/translation_stats.py
@@ -15,10 +15,10 @@ def calculate_coverage(filename):
     lines_uncovered = 0
 
     for line in lines:
-        if line.startswith("msgid "):
+        if line.startswith('msgid '):
             lines_count += 1
 
-        elif line.startswith("msgstr"):
+        elif line.startswith('msgstr'):
             if line.startswith('msgstr ""') or line.startswith("msgstr ''"):
                 lines_uncovered += 1
             else:
@@ -49,7 +49,7 @@ if __name__ == '__main__':
                 locales[locale] = locale_file
 
     if verbose:
-        print("-" * 16)
+        print('-' * 16)
 
     percentages = []
 
@@ -72,7 +72,7 @@ if __name__ == '__main__':
         percentages.append(percentage)
 
     if verbose:
-        print("-" * 16)
+        print('-' * 16)
 
     # write locale stats
     with open(STAT_FILE, 'w') as target:
@@ -83,4 +83,4 @@ if __name__ == '__main__':
     else:
         avg = 0
 
-    print(f"InvenTree translation coverage: {avg}%")
+    print(f'InvenTree translation coverage: {avg}%')
diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py
index 45a74f533e..d31bf9e1a4 100644
--- a/InvenTree/stock/admin.py
+++ b/InvenTree/stock/admin.py
@@ -110,7 +110,7 @@ class LocationTypeAdmin(admin.ModelAdmin):
         return (
             super()
             .get_queryset(request)
-            .annotate(location_count=Count("stock_locations"))
+            .annotate(location_count=Count('stock_locations'))
         )
 
     def location_count(self, obj):
diff --git a/InvenTree/stock/api.py b/InvenTree/stock/api.py
index d4b7ade93e..a01e4e9fbf 100644
--- a/InvenTree/stock/api.py
+++ b/InvenTree/stock/api.py
@@ -283,7 +283,7 @@ class StockLocationList(APIDownloadMixin, ListCreateAPI):
         """Download the filtered queryset as a data file"""
         dataset = LocationResource().export(queryset=queryset)
         filedata = dataset.export(export_format)
-        filename = f"InvenTree_Locations.{export_format}"
+        filename = f'InvenTree_Locations.{export_format}'
 
         return DownloadFile(filedata, filename)
 
@@ -389,11 +389,11 @@ class StockLocationTypeList(ListCreateAPI):
 
     filter_backends = SEARCH_ORDER_FILTER
 
-    ordering_fields = ["name", "location_count", "icon"]
+    ordering_fields = ['name', 'location_count', 'icon']
 
-    ordering = ["-location_count"]
+    ordering = ['-location_count']
 
-    search_fields = ["name"]
+    search_fields = ['name']
 
     def get_queryset(self):
         """Override the queryset method to include location count."""
@@ -491,9 +491,9 @@ class StockFilter(rest_filters.FilterSet):
     )
 
     # Part attribute filters
-    assembly = rest_filters.BooleanFilter(label="Assembly", field_name='part__assembly')
-    active = rest_filters.BooleanFilter(label="Active", field_name='part__active')
-    salable = rest_filters.BooleanFilter(label="Salable", field_name='part__salable')
+    assembly = rest_filters.BooleanFilter(label='Assembly', field_name='part__assembly')
+    active = rest_filters.BooleanFilter(label='Active', field_name='part__active')
+    salable = rest_filters.BooleanFilter(label='Salable', field_name='part__salable')
 
     min_stock = rest_filters.NumberFilter(
         label='Minimum stock', field_name='quantity', lookup_expr='gte'
@@ -570,14 +570,14 @@ class StockFilter(rest_filters.FilterSet):
         return queryset.filter(Q(quantity__lte=F('allocated')))
 
     batch = rest_filters.CharFilter(
-        label="Batch code filter (case insensitive)", lookup_expr='iexact'
+        label='Batch code filter (case insensitive)', lookup_expr='iexact'
     )
 
     batch_regex = rest_filters.CharFilter(
-        label="Batch code filter (regex)", field_name='batch', lookup_expr='iregex'
+        label='Batch code filter (regex)', field_name='batch', lookup_expr='iregex'
     )
 
-    is_building = rest_filters.BooleanFilter(label="In production")
+    is_building = rest_filters.BooleanFilter(label='In production')
 
     # Serial number filtering
     serial_gte = rest_filters.NumberFilter(
@@ -741,7 +741,7 @@ class StockFilter(rest_filters.FilterSet):
 
     # Stock "expiry" filters
     expiry_date_lte = InvenTreeDateFilter(
-        label=_("Expiry date before"), field_name='expiry_date', lookup_expr='lte'
+        label=_('Expiry date before'), field_name='expiry_date', lookup_expr='lte'
     )
 
     expiry_date_gte = InvenTreeDateFilter(
@@ -913,7 +913,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
             if not part.trackable:
                 raise ValidationError({
                     'serial_numbers': [
-                        _("Serial numbers cannot be supplied for a non-trackable part")
+                        _('Serial numbers cannot be supplied for a non-trackable part')
                     ]
                 })
 
@@ -939,9 +939,9 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
                             errors.append(exc.message)
 
                 if len(errors) > 0:
-                    msg = _("The following serial numbers already exist or are invalid")
-                    msg += " : "
-                    msg += ",".join([str(e) for e in invalid])
+                    msg = _('The following serial numbers already exist or are invalid')
+                    msg += ' : '
+                    msg += ','.join([str(e) for e in invalid])
 
                     raise ValidationError({'serial_numbers': errors + [msg]})
 
@@ -1098,7 +1098,7 @@ class StockList(APIDownloadMixin, ListCreateDestroyAPIView):
                     queryset = queryset.filter(part=part)
 
             except (ValueError, Part.DoesNotExist):
-                raise ValidationError({"part": "Invalid Part ID specified"})
+                raise ValidationError({'part': 'Invalid Part ID specified'})
 
         # Does the client wish to filter by stock location?
         loc_id = params.get('location', None)
@@ -1531,7 +1531,7 @@ stock_api_urls = [
                 ]),
             ),
             re_path(
-                r'^.*$', StockLocationTypeList.as_view(), name="api-location-type-list"
+                r'^.*$', StockLocationTypeList.as_view(), name='api-location-type-list'
             ),
         ]),
     ),
diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py
index b0fd9f4b28..af29e74fac 100644
--- a/InvenTree/stock/models.py
+++ b/InvenTree/stock/models.py
@@ -62,8 +62,8 @@ class StockLocationType(MetadataMixin, models.Model):
     class Meta:
         """Metaclass defines extra model properties."""
 
-        verbose_name = _("Stock Location type")
-        verbose_name_plural = _("Stock Location types")
+        verbose_name = _('Stock Location type')
+        verbose_name_plural = _('Stock Location types')
 
     @staticmethod
     def get_api_url():
@@ -75,21 +75,21 @@ class StockLocationType(MetadataMixin, models.Model):
         return self.name
 
     name = models.CharField(
-        blank=False, max_length=100, verbose_name=_("Name"), help_text=_("Name")
+        blank=False, max_length=100, verbose_name=_('Name'), help_text=_('Name')
     )
 
     description = models.CharField(
         blank=True,
         max_length=250,
-        verbose_name=_("Description"),
-        help_text=_("Description (optional)"),
+        verbose_name=_('Description'),
+        help_text=_('Description (optional)'),
     )
 
     icon = models.CharField(
         blank=True,
         max_length=100,
-        verbose_name=_("Icon"),
-        help_text=_("Default icon for all locations that have no icon set (optional)"),
+        verbose_name=_('Icon'),
+        help_text=_('Default icon for all locations that have no icon set (optional)'),
     )
 
 
@@ -104,7 +104,7 @@ class StockLocationManager(TreeManager):
 
         - Joins the StockLocationType by default for speedier icon access
         """
-        return super().get_queryset().select_related("location_type")
+        return super().get_queryset().select_related('location_type')
 
 
 class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
@@ -145,9 +145,9 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
     custom_icon = models.CharField(
         blank=True,
         max_length=100,
-        verbose_name=_("Icon"),
-        help_text=_("Icon (optional)"),
-        db_column="icon",
+        verbose_name=_('Icon'),
+        help_text=_('Icon (optional)'),
+        db_column='icon',
     )
 
     owner = models.ForeignKey(
@@ -178,11 +178,11 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
     location_type = models.ForeignKey(
         StockLocationType,
         on_delete=models.SET_NULL,
-        verbose_name=_("Location type"),
-        related_name="stock_locations",
+        verbose_name=_('Location type'),
+        related_name='stock_locations',
         null=True,
         blank=True,
-        help_text=_("Stock location type of this location"),
+        help_text=_('Stock location type of this location'),
     )
 
     @property
@@ -197,7 +197,7 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
         if self.location_type:
             return self.location_type.icon
 
-        return ""
+        return ''
 
     @icon.setter
     def icon(self, value):
@@ -250,8 +250,8 @@ class StockLocation(InvenTreeBarcodeMixin, MetadataMixin, InvenTreeTree):
         if self.pk and self.structural and self.stock_item_count(False) > 0:
             raise ValidationError(
                 _(
-                    "You cannot make this stock location structural because some stock items "
-                    "are already located into it!"
+                    'You cannot make this stock location structural because some stock items '
+                    'are already located into it!'
                 )
             )
         super().clean()
@@ -614,7 +614,7 @@ class StockItem(
         if self.location is not None and self.location.structural:
             raise ValidationError({
                 'location': _(
-                    "Stock items cannot be located into structural stock locations!"
+                    'Stock items cannot be located into structural stock locations!'
                 )
             })
 
@@ -644,7 +644,7 @@ class StockItem(
             # Virtual parts cannot have stock items created against them
             if self.part.virtual:
                 raise ValidationError({
-                    'part': _("Stock item cannot be created for virtual parts")
+                    'part': _('Stock item cannot be created for virtual parts')
                 })
         except PartModels.Part.DoesNotExist:
             # For some reason the 'clean' process sometimes throws errors because self.part does not exist
@@ -703,7 +703,7 @@ class StockItem(
         # If the item is marked as "is_building", it must point to a build!
         if self.is_building and not self.build:
             raise ValidationError({
-                'build': _("Item must have a build reference if is_building=True")
+                'build': _('Item must have a build reference if is_building=True')
             })
 
         # If the item points to a build, check that the Part references match
@@ -716,7 +716,7 @@ class StockItem(
                 pass
             else:
                 raise ValidationError({
-                    'build': _("Build reference does not point to the same part object")
+                    'build': _('Build reference does not point to the same part object')
                 })
 
     def get_absolute_url(self):
@@ -793,8 +793,8 @@ class StockItem(
         blank=True,
         limit_choices_to={'is_customer': True},
         related_name='assigned_stock',
-        help_text=_("Customer"),
-        verbose_name=_("Customer"),
+        help_text=_('Customer'),
+        verbose_name=_('Customer'),
     )
 
     serial = models.CharField(
@@ -808,7 +808,7 @@ class StockItem(
     serial_int = models.IntegerField(default=0)
 
     link = InvenTreeURLField(
-        verbose_name=_('External Link'), blank=True, help_text=_("Link to external URL")
+        verbose_name=_('External Link'), blank=True, help_text=_('Link to external URL')
     )
 
     batch = models.CharField(
@@ -821,7 +821,7 @@ class StockItem(
     )
 
     quantity = models.DecimalField(
-        verbose_name=_("Stock Quantity"),
+        verbose_name=_('Stock Quantity'),
         max_digits=15,
         decimal_places=5,
         validators=[MinValueValidator(0)],
@@ -863,7 +863,7 @@ class StockItem(
     sales_order = models.ForeignKey(
         'order.SalesOrder',
         on_delete=models.SET_NULL,
-        verbose_name=_("Destination Sales Order"),
+        verbose_name=_('Destination Sales Order'),
         related_name='stock_items',
         null=True,
         blank=True,
@@ -1454,32 +1454,32 @@ class StockItem(
             return
 
         if not self.part.trackable:
-            raise ValidationError({"part": _("Part is not set as trackable")})
+            raise ValidationError({'part': _('Part is not set as trackable')})
 
         # Quantity must be a valid integer value
         try:
             quantity = int(quantity)
         except ValueError:
-            raise ValidationError({"quantity": _("Quantity must be integer")})
+            raise ValidationError({'quantity': _('Quantity must be integer')})
 
         if quantity <= 0:
-            raise ValidationError({"quantity": _("Quantity must be greater than zero")})
+            raise ValidationError({'quantity': _('Quantity must be greater than zero')})
 
         if quantity > self.quantity:
             raise ValidationError({
-                "quantity": _(
-                    f"Quantity must not exceed available stock quantity ({self.quantity})"
+                'quantity': _(
+                    f'Quantity must not exceed available stock quantity ({self.quantity})'
                 )
             })
 
         if type(serials) not in [list, tuple]:
             raise ValidationError({
-                "serial_numbers": _("Serial numbers must be a list of integers")
+                'serial_numbers': _('Serial numbers must be a list of integers')
             })
 
         if quantity != len(serials):
             raise ValidationError({
-                "quantity": _("Quantity does not match serial numbers")
+                'quantity': _('Quantity does not match serial numbers')
             })
 
         # Test if each of the serial numbers are valid
@@ -1487,8 +1487,8 @@ class StockItem(
 
         if len(existing) > 0:
             exists = ','.join([str(x) for x in existing])
-            msg = _("Serial numbers already exist") + f": {exists}"
-            raise ValidationError({"serial_numbers": msg})
+            msg = _('Serial numbers already exist') + f': {exists}'
+            raise ValidationError({'serial_numbers': msg})
 
         # Create a new stock item for each unique serial number
         for serial in serials:
@@ -1570,7 +1570,7 @@ class StockItem(
                 raise ValidationError(_('Stock item is currently in production'))
 
             if self.serialized:
-                raise ValidationError(_("Serialized stock cannot be merged"))
+                raise ValidationError(_('Serialized stock cannot be merged'))
 
             if other:
                 # Specific checks (rely on the 'other' part)
@@ -1581,7 +1581,7 @@ class StockItem(
 
                 # Base part must match
                 if self.part != other.part:
-                    raise ValidationError(_("Stock items must refer to the same part"))
+                    raise ValidationError(_('Stock items must refer to the same part'))
 
                 # Check if supplier part references match
                 if (
@@ -1589,12 +1589,12 @@ class StockItem(
                     and not allow_mismatched_suppliers
                 ):
                     raise ValidationError(
-                        _("Stock items must refer to the same supplier part")
+                        _('Stock items must refer to the same supplier part')
                     )
 
                 # Check if stock status codes match
                 if self.status != other.status and not allow_mismatched_status:
-                    raise ValidationError(_("Stock status codes must match"))
+                    raise ValidationError(_('Stock status codes must match'))
 
         except ValidationError as e:
             if raise_error:
@@ -1781,7 +1781,7 @@ class StockItem(
             return False
 
         if not self.in_stock:
-            raise ValidationError(_("StockItem cannot be moved as it is not in stock"))
+            raise ValidationError(_('StockItem cannot be moved as it is not in stock'))
 
         if quantity <= 0:
             return False
@@ -1957,7 +1957,7 @@ class StockItem(
             s += f' @ {self.location.name}'
 
         if self.purchase_order:
-            s += f" ({self.purchase_order})"
+            s += f' ({self.purchase_order})'
 
         return s
 
@@ -2182,7 +2182,7 @@ class StockItemAttachment(InvenTreeAttachment):
 
     def getSubdir(self):
         """Override attachment location."""
-        return os.path.join("stock_files", str(self.stock_item.id))
+        return os.path.join('stock_files', str(self.stock_item.id))
 
     stock_item = models.ForeignKey(
         StockItem, on_delete=models.CASCADE, related_name='attachments'
@@ -2297,13 +2297,13 @@ class StockItemTestResult(MetadataMixin, models.Model):
                 if template.requires_value:
                     if not self.value:
                         raise ValidationError({
-                            "value": _("Value must be provided for this test")
+                            'value': _('Value must be provided for this test')
                         })
 
                 if template.requires_attachment:
                     if not self.attachment:
                         raise ValidationError({
-                            "attachment": _("Attachment must be uploaded for this test")
+                            'attachment': _('Attachment must be uploaded for this test')
                         })
 
                 break
@@ -2341,7 +2341,7 @@ class StockItemTestResult(MetadataMixin, models.Model):
     )
 
     notes = models.CharField(
-        blank=True, max_length=500, verbose_name=_('Notes'), help_text=_("Test notes")
+        blank=True, max_length=500, verbose_name=_('Notes'), help_text=_('Test notes')
     )
 
     user = models.ForeignKey(User, on_delete=models.SET_NULL, blank=True, null=True)
diff --git a/InvenTree/stock/serializers.py b/InvenTree/stock/serializers.py
index 952b7b63ef..f01b71f954 100644
--- a/InvenTree/stock/serializers.py
+++ b/InvenTree/stock/serializers.py
@@ -115,7 +115,7 @@ class StockItemSerializerBrief(InvenTree.serializers.InvenTreeModelSerializer):
     def validate_serial(self, value):
         """Make sure serial is not to big."""
         if abs(extract_int(value)) > 0x7FFFFFFF:
-            raise serializers.ValidationError(_("Serial number is too large"))
+            raise serializers.ValidationError(_('Serial number is too large'))
         return value
 
 
@@ -196,8 +196,8 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
         queryset=part_models.Part.objects.all(),
         many=False,
         allow_null=False,
-        help_text=_("Base Part"),
-        label=_("Part"),
+        help_text=_('Base Part'),
+        label=_('Part'),
     )
 
     location_path = serializers.ListField(
@@ -212,15 +212,15 @@ class StockItemSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
         required=False,
         allow_null=True,
         help_text=_(
-            "Use pack size when adding: the quantity defined is the number of packs"
+            'Use pack size when adding: the quantity defined is the number of packs'
         ),
-        label=("Use pack size"),
+        label=('Use pack size'),
     )
 
     def validate_part(self, part):
         """Ensure the provided Part instance is valid"""
         if part.virtual:
-            raise ValidationError(_("Stock item cannot be created for virtual parts"))
+            raise ValidationError(_('Stock item cannot be created for virtual parts'))
 
         return part
 
@@ -391,12 +391,12 @@ class SerializeStockItemSerializer(serializers.Serializer):
         item = self.context['item']
 
         if quantity < 0:
-            raise ValidationError(_("Quantity must be greater than zero"))
+            raise ValidationError(_('Quantity must be greater than zero'))
 
         if quantity > item.quantity:
             q = item.quantity
             raise ValidationError(
-                _(f"Quantity must not exceed available stock quantity ({q})")
+                _(f'Quantity must not exceed available stock quantity ({q})')
             )
 
         return quantity
@@ -420,8 +420,8 @@ class SerializeStockItemSerializer(serializers.Serializer):
     notes = serializers.CharField(
         required=False,
         allow_blank=True,
-        label=_("Notes"),
-        help_text=_("Optional note field"),
+        label=_('Notes'),
+        help_text=_('Optional note field'),
     )
 
     def validate(self, data):
@@ -431,7 +431,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
         item = self.context['item']
 
         if not item.part.trackable:
-            raise ValidationError(_("Serial numbers cannot be assigned to this part"))
+            raise ValidationError(_('Serial numbers cannot be assigned to this part'))
 
         # Ensure the serial numbers are valid!
         quantity = data['quantity']
@@ -448,7 +448,7 @@ class SerializeStockItemSerializer(serializers.Serializer):
 
         if len(existing) > 0:
             exists = ','.join([str(x) for x in existing])
-            error = _('Serial numbers already exist') + ": " + exists
+            error = _('Serial numbers already exist') + ': ' + exists
 
             raise ValidationError({'serial_numbers': error})
 
@@ -508,7 +508,7 @@ class InstallStockItemSerializer(serializers.Serializer):
         """Validate the quantity value."""
 
         if quantity < 1:
-            raise ValidationError(_("Quantity to install must be at least 1"))
+            raise ValidationError(_('Quantity to install must be at least 1'))
 
         return quantity
 
@@ -516,14 +516,14 @@ class InstallStockItemSerializer(serializers.Serializer):
         """Validate the selected stock item."""
         if not stock_item.in_stock:
             # StockItem must be in stock to be "installed"
-            raise ValidationError(_("Stock item is unavailable"))
+            raise ValidationError(_('Stock item is unavailable'))
 
         parent_item = self.context['item']
         parent_part = parent_item.part
 
         # Check if the selected part is in the Bill of Materials of the parent item
         if not parent_part.check_if_part_in_bom(stock_item.part):
-            raise ValidationError(_("Selected part is not in the Bill of Materials"))
+            raise ValidationError(_('Selected part is not in the Bill of Materials'))
 
         return stock_item
 
@@ -536,7 +536,7 @@ class InstallStockItemSerializer(serializers.Serializer):
 
         if quantity > stock_item.quantity:
             raise ValidationError(
-                _("Quantity to install must not exceed available quantity")
+                _('Quantity to install must not exceed available quantity')
             )
 
         return data
@@ -619,7 +619,7 @@ class ConvertStockItemSerializer(serializers.Serializer):
 
         if part not in valid_options:
             raise ValidationError(
-                _("Selected part is not a valid option for conversion")
+                _('Selected part is not a valid option for conversion')
             )
 
         return part
@@ -635,7 +635,7 @@ class ConvertStockItemSerializer(serializers.Serializer):
 
         if stock_item.supplier_part is not None:
             raise ValidationError(
-                _("Cannot convert stock item with assigned SupplierPart")
+                _('Cannot convert stock item with assigned SupplierPart')
             )
 
         return data
@@ -709,7 +709,7 @@ class StockChangeStatusSerializer(serializers.Serializer):
     def validate_items(self, items):
         """Validate the selected stock items"""
         if len(items) == 0:
-            raise ValidationError(_("No stock items selected"))
+            raise ValidationError(_('No stock items selected'))
 
         return items
 
@@ -784,16 +784,16 @@ class StockLocationTypeSerializer(InvenTree.serializers.InvenTreeModelSerializer
         """Serializer metaclass."""
 
         model = StockLocationType
-        fields = ["pk", "name", "description", "icon", "location_count"]
+        fields = ['pk', 'name', 'description', 'icon', 'location_count']
 
-        read_only_fields = ["location_count"]
+        read_only_fields = ['location_count']
 
     location_count = serializers.IntegerField(read_only=True)
 
     @staticmethod
     def annotate_queryset(queryset):
         """Add location count to each location type."""
-        return queryset.annotate(location_count=Count("stock_locations"))
+        return queryset.annotate(location_count=Count('stock_locations'))
 
 
 class LocationTreeSerializer(InvenTree.serializers.InvenTreeModelSerializer):
@@ -870,7 +870,7 @@ class LocationSerializer(InvenTree.serializers.InvenTreeTagModelSerializer):
 
     # Detail for location type
     location_type_detail = StockLocationTypeSerializer(
-        source="location_type", read_only=True, many=False
+        source='location_type', read_only=True, many=False
     )
 
 
@@ -967,19 +967,19 @@ class StockAssignmentItemSerializer(serializers.Serializer):
         """
         # The item must currently be "in stock"
         if not item.in_stock:
-            raise ValidationError(_("Item must be in stock"))
+            raise ValidationError(_('Item must be in stock'))
 
         # The base part must be "salable"
         if not item.part.salable:
-            raise ValidationError(_("Part must be salable"))
+            raise ValidationError(_('Part must be salable'))
 
         # The item must not be allocated to a sales order
         if item.sales_order_allocations.count() > 0:
-            raise ValidationError(_("Item is allocated to a sales order"))
+            raise ValidationError(_('Item is allocated to a sales order'))
 
         # The item must not be allocated to a build order
         if item.allocations.count() > 0:
-            raise ValidationError(_("Item is allocated to a build order"))
+            raise ValidationError(_('Item is allocated to a build order'))
 
         return item
 
@@ -1027,7 +1027,7 @@ class StockAssignmentSerializer(serializers.Serializer):
         items = data.get('items', [])
 
         if len(items) == 0:
-            raise ValidationError(_("A list of stock items must be provided"))
+            raise ValidationError(_('A list of stock items must be provided'))
 
         return data
 
@@ -1262,8 +1262,8 @@ class StockAdjustmentSerializer(serializers.Serializer):
     notes = serializers.CharField(
         required=False,
         allow_blank=True,
-        label=_("Notes"),
-        help_text=_("Stock transaction notes"),
+        label=_('Notes'),
+        help_text=_('Stock transaction notes'),
     )
 
     def validate(self, data):
@@ -1273,7 +1273,7 @@ class StockAdjustmentSerializer(serializers.Serializer):
         items = data.get('items', [])
 
         if len(items) == 0:
-            raise ValidationError(_("A list of stock items must be provided"))
+            raise ValidationError(_('A list of stock items must be provided'))
 
         return data
 
diff --git a/InvenTree/stock/test_api.py b/InvenTree/stock/test_api.py
index 6070ff4ab9..349e4574e9 100644
--- a/InvenTree/stock/test_api.py
+++ b/InvenTree/stock/test_api.py
@@ -222,7 +222,7 @@ class StockLocationTest(StockAPITestCase):
             for jj in range(3):
                 stock_items.append(
                     StockItem.objects.create(
-                        batch=f"Batch xyz {jj}",
+                        batch=f'Batch xyz {jj}',
                         location=stock_location_to_delete,
                         part=part,
                     )
@@ -233,8 +233,8 @@ class StockLocationTest(StockAPITestCase):
             # Create sub location under the stock location to be deleted
             for ii in range(3):
                 child = StockLocation.objects.create(
-                    name=f"Sub-location {ii}",
-                    description="A sub-location of the deleted stock location",
+                    name=f'Sub-location {ii}',
+                    description='A sub-location of the deleted stock location',
                     parent=stock_location_to_delete,
                 )
                 child_stock_locations.append(child)
@@ -243,7 +243,7 @@ class StockLocationTest(StockAPITestCase):
                 for jj in range(3):
                     child_stock_locations_items.append(
                         StockItem.objects.create(
-                            batch=f"B xyz {jj}", part=part, location=child
+                            batch=f'B xyz {jj}', part=part, location=child
                         )
                     )
 
@@ -314,7 +314,7 @@ class StockLocationTest(StockAPITestCase):
         # Make sure that we get an error if we try to create a stock item in the structural location
         with self.assertRaises(ValidationError):
             item = StockItem.objects.create(
-                batch="Stock item which shall not be created",
+                batch='Stock item which shall not be created',
                 location=structural_location,
             )
 
@@ -339,7 +339,7 @@ class StockLocationTest(StockAPITestCase):
 
         # Create the test stock item located to a non-structural category
         item = StockItem.objects.create(
-            batch="BBB", location=non_structural_location, part=part
+            batch='BBB', location=non_structural_location, part=part
         )
 
         # Try to relocate it to a structural location
@@ -358,99 +358,99 @@ class StockLocationTest(StockAPITestCase):
 
     def test_stock_location_icon(self):
         """Test stock location icon inheritance from StockLocationType."""
-        parent_location = StockLocation.objects.create(name="Parent location")
+        parent_location = StockLocation.objects.create(name='Parent location')
 
         location_type = StockLocationType.objects.create(
-            name="Box", description="This is a very cool type of box", icon="fas fa-box"
+            name='Box', description='This is a very cool type of box', icon='fas fa-box'
         )
         location = StockLocation.objects.create(
-            name="Test location",
-            custom_icon="fas fa-microscope",
+            name='Test location',
+            custom_icon='fas fa-microscope',
             location_type=location_type,
             parent=parent_location,
         )
 
         res = self.get(
-            self.list_url, {"parent": str(parent_location.pk)}, expected_code=200
+            self.list_url, {'parent': str(parent_location.pk)}, expected_code=200
         ).json()
         self.assertEqual(
-            res[0]["icon"],
-            "fas fa-microscope",
-            "Custom icon from location should be returned",
+            res[0]['icon'],
+            'fas fa-microscope',
+            'Custom icon from location should be returned',
         )
 
-        location.custom_icon = ""
+        location.custom_icon = ''
         location.save()
         res = self.get(
-            self.list_url, {"parent": str(parent_location.pk)}, expected_code=200
+            self.list_url, {'parent': str(parent_location.pk)}, expected_code=200
         ).json()
         self.assertEqual(
-            res[0]["icon"],
-            "fas fa-box",
-            "Custom icon is None, therefore it should inherit the location type icon",
+            res[0]['icon'],
+            'fas fa-box',
+            'Custom icon is None, therefore it should inherit the location type icon',
         )
 
-        location_type.icon = ""
+        location_type.icon = ''
         location_type.save()
         res = self.get(
-            self.list_url, {"parent": str(parent_location.pk)}, expected_code=200
+            self.list_url, {'parent': str(parent_location.pk)}, expected_code=200
         ).json()
         self.assertEqual(
-            res[0]["icon"],
-            "",
-            "Custom icon and location type icon is None, None should be returned",
+            res[0]['icon'],
+            '',
+            'Custom icon and location type icon is None, None should be returned',
         )
 
     def test_stock_location_list_filter(self):
         """Test stock location list filters."""
-        parent_location = StockLocation.objects.create(name="Parent location")
+        parent_location = StockLocation.objects.create(name='Parent location')
 
         location_type = StockLocationType.objects.create(
-            name="Box", description="This is a very cool type of box", icon="fas fa-box"
+            name='Box', description='This is a very cool type of box', icon='fas fa-box'
         )
         location_type2 = StockLocationType.objects.create(
-            name="Shelf",
-            description="This is a very cool type of shelf",
-            icon="fas fa-shapes",
+            name='Shelf',
+            description='This is a very cool type of shelf',
+            icon='fas fa-shapes',
         )
         StockLocation.objects.create(
-            name="Test location w. type",
+            name='Test location w. type',
             location_type=location_type,
             parent=parent_location,
         )
         StockLocation.objects.create(
-            name="Test location w. type 2",
+            name='Test location w. type 2',
             parent=parent_location,
             location_type=location_type2,
         )
         StockLocation.objects.create(
-            name="Test location wo type", parent=parent_location
+            name='Test location wo type', parent=parent_location
         )
 
         res = self.get(
             self.list_url,
-            {"parent": str(parent_location.pk), "has_location_type": "1"},
+            {'parent': str(parent_location.pk), 'has_location_type': '1'},
             expected_code=200,
         ).json()
         self.assertEqual(len(res), 2)
-        self.assertEqual(res[0]["name"], "Test location w. type")
-        self.assertEqual(res[1]["name"], "Test location w. type 2")
+        self.assertEqual(res[0]['name'], 'Test location w. type')
+        self.assertEqual(res[1]['name'], 'Test location w. type 2')
 
         res = self.get(
             self.list_url,
-            {"parent": str(parent_location.pk), "location_type": str(location_type.pk)},
+            {'parent': str(parent_location.pk), 'location_type': str(location_type.pk)},
             expected_code=200,
         ).json()
         self.assertEqual(len(res), 1)
-        self.assertEqual(res[0]["name"], "Test location w. type")
+        self.assertEqual(res[0]['name'], 'Test location w. type')
 
         res = self.get(
             self.list_url,
-            {"parent": str(parent_location.pk), "has_location_type": "0"},
+            {'parent': str(parent_location.pk), 'has_location_type': '0'},
             expected_code=200,
         ).json()
         self.assertEqual(len(res), 1)
-        self.assertEqual(res[0]["name"], "Test location wo type")
+        self.assertEqual(res[0]['name'], 'Test location wo type')
 
 
 class StockLocationTypeTest(StockAPITestCase):
@@ -462,31 +462,31 @@ class StockLocationTypeTest(StockAPITestCase):
         """Test that the list endpoint works as expected."""
         location_types = [
             StockLocationType.objects.create(
-                name="Type 1", description="Type 1 desc", icon="fas fa-box"
+                name='Type 1', description='Type 1 desc', icon='fas fa-box'
             ),
             StockLocationType.objects.create(
-                name="Type 2", description="Type 2 desc", icon="fas fa-box"
+                name='Type 2', description='Type 2 desc', icon='fas fa-box'
             ),
             StockLocationType.objects.create(
-                name="Type 3", description="Type 3 desc", icon="fas fa-box"
+                name='Type 3', description='Type 3 desc', icon='fas fa-box'
             ),
         ]
 
-        StockLocation.objects.create(name="Loc 1", location_type=location_types[0])
-        StockLocation.objects.create(name="Loc 2", location_type=location_types[0])
-        StockLocation.objects.create(name="Loc 3", location_type=location_types[1])
+        StockLocation.objects.create(name='Loc 1', location_type=location_types[0])
+        StockLocation.objects.create(name='Loc 2', location_type=location_types[0])
+        StockLocation.objects.create(name='Loc 3', location_type=location_types[1])
 
         res = self.get(self.list_url, expected_code=200).json()
         self.assertEqual(len(res), 3)
-        self.assertCountEqual([r["location_count"] for r in res], [2, 1, 0])
+        self.assertCountEqual([r['location_count'] for r in res], [2, 1, 0])
 
     def test_delete(self):
         """Test that we can delete a location type via API."""
         location_type = StockLocationType.objects.create(
-            name="Type 1", description="Type 1 desc", icon="fas fa-box"
+            name='Type 1', description='Type 1 desc', icon='fas fa-box'
         )
         self.delete(
-            reverse('api-location-type-detail', kwargs={"pk": location_type.pk}),
+            reverse('api-location-type-detail', kwargs={'pk': location_type.pk}),
             expected_code=204,
         )
         self.assertEqual(StockLocationType.objects.count(), 0)
@@ -495,24 +495,24 @@ class StockLocationTypeTest(StockAPITestCase):
         """Test that we can create a location type via API."""
         self.post(
             self.list_url,
-            {"name": "Test Type 1", "description": "Test desc 1", "icon": "fas fa-box"},
+            {'name': 'Test Type 1', 'description': 'Test desc 1', 'icon': 'fas fa-box'},
             expected_code=201,
         )
         self.assertIsNotNone(
-            StockLocationType.objects.filter(name="Test Type 1").first()
+            StockLocationType.objects.filter(name='Test Type 1').first()
         )
 
     def test_update(self):
         """Test that we can update a location type via API."""
         location_type = StockLocationType.objects.create(
-            name="Type 1", description="Type 1 desc", icon="fas fa-box"
+            name='Type 1', description='Type 1 desc', icon='fas fa-box'
         )
         res = self.patch(
-            reverse('api-location-type-detail', kwargs={"pk": location_type.pk}),
-            {"icon": "fas fa-shapes"},
+            reverse('api-location-type-detail', kwargs={'pk': location_type.pk}),
+            {'icon': 'fas fa-shapes'},
             expected_code=200,
         ).json()
-        self.assertEqual(res["icon"], "fas fa-shapes")
+        self.assertEqual(res['icon'], 'fas fa-shapes')
 
 
 class StockItemListTest(StockAPITestCase):
@@ -573,7 +573,7 @@ class StockItemListTest(StockAPITestCase):
 
     def test_filter_by_ipn(self):
         """Filter StockItem by IPN reference."""
-        response = self.get_stock(IPN="R.CH")
+        response = self.get_stock(IPN='R.CH')
         self.assertEqual(len(response), 3)
 
     def test_filter_by_location(self):
@@ -850,30 +850,30 @@ class StockItemListTest(StockAPITestCase):
 
         # 3 items when just filtering by part
         response = self.get(
-            url, {"part": component.pk, "in_stock": True}, expected_code=200
+            url, {'part': component.pk, 'in_stock': True}, expected_code=200
         )
         self.assertEqual(len(response.data), 3)
 
         # 1 item when filtering by "not allocated"
         response = self.get(
             url,
-            {"part": component.pk, "in_stock": True, "allocated": False},
+            {'part': component.pk, 'in_stock': True, 'allocated': False},
             expected_code=200,
         )
 
         self.assertEqual(len(response.data), 1)
-        self.assertEqual(response.data[0]["pk"], stock_3.pk)
+        self.assertEqual(response.data[0]['pk'], stock_3.pk)
 
         # 2 items when filtering by "allocated"
         response = self.get(
             url,
-            {"part": component.pk, "in_stock": True, "allocated": True},
+            {'part': component.pk, 'in_stock': True, 'allocated': True},
             expected_code=200,
         )
 
         self.assertEqual(len(response.data), 2)
 
-        ids = [item["pk"] for item in response.data]
+        ids = [item['pk'] for item in response.data]
 
         self.assertIn(stock_1.pk, ids)
         self.assertIn(stock_2.pk, ids)
@@ -1253,7 +1253,7 @@ class StockItemTest(StockAPITestCase):
         # Now, try to install an item which *is* in the BOM for the parent part
         response = self.post(
             url,
-            {'stock_item': sub_item.pk, 'note': "This time, it should be good!"},
+            {'stock_item': sub_item.pk, 'note': 'This time, it should be good!'},
             expected_code=201,
         )
 
@@ -1333,8 +1333,8 @@ class StockItemTest(StockAPITestCase):
         for color in ['Red', 'Green', 'Blue', 'Yellow', 'Pink', 'Black']:
             variants.append(
                 part.models.Part.objects.create(
-                    name=f"{color} Variant",
-                    description="Variant part with a specific color",
+                    name=f'{color} Variant',
+                    description='Variant part with a specific color',
                     variant_of=master_part,
                     category=category,
                 )
@@ -1416,7 +1416,7 @@ class StocktakeTest(StockAPITestCase):
             # POST with a valid action
             response = self.post(url, data)
 
-            self.assertIn("This field is required", str(response.data["items"]))
+            self.assertIn('This field is required', str(response.data['items']))
 
             data['items'] = [{'no': 'aa'}]
 
@@ -1456,7 +1456,7 @@ class StocktakeTest(StockAPITestCase):
                 status_code=status.HTTP_400_BAD_REQUEST,
             )
 
-            data['items'] = [{'pk': 1234, 'quantity': "-1.234"}]
+            data['items'] = [{'pk': 1234, 'quantity': '-1.234'}]
 
             response = self.post(url, data)
             self.assertContains(
@@ -1470,7 +1470,7 @@ class StocktakeTest(StockAPITestCase):
         data = {
             'items': [{'pk': 1234, 'quantity': 10}],
             'location': 1,
-            'notes': "Moving to a new location",
+            'notes': 'Moving to a new location',
         }
 
         url = reverse('api-stock-transfer')
@@ -1602,7 +1602,7 @@ class StockTestResultTest(StockAPITestCase):
                 'result': False,
                 'value': '150kPa',
                 'notes': 'I guess there was just too much pressure?',
-                "attachment": bitmap,
+                'attachment': bitmap,
             }
 
             response = self.client.post(self.get_url(), data)
@@ -1625,7 +1625,7 @@ class StockTestResultTest(StockAPITestCase):
                 url,
                 {
                     'stock_item': 1,
-                    'test': f"Some test {_ii}",
+                    'test': f'Some test {_ii}',
                     'result': True,
                     'value': 'Test result value',
                 },
diff --git a/InvenTree/stock/tests.py b/InvenTree/stock/tests.py
index b88ec9679d..8b44c9774d 100644
--- a/InvenTree/stock/tests.py
+++ b/InvenTree/stock/tests.py
@@ -54,10 +54,10 @@ class StockTest(StockTestBase):
 
     def test_pathstring(self):
         """Check that pathstring updates occur as expected"""
-        a = StockLocation.objects.create(name="A")
-        b = StockLocation.objects.create(name="B", parent=a)
-        c = StockLocation.objects.create(name="C", parent=b)
-        d = StockLocation.objects.create(name="D", parent=c)
+        a = StockLocation.objects.create(name='A')
+        b = StockLocation.objects.create(name='B', parent=a)
+        c = StockLocation.objects.create(name='C', parent=b)
+        d = StockLocation.objects.create(name='D', parent=c)
 
         def refresh():
             a.refresh_from_db()
@@ -66,56 +66,56 @@ class StockTest(StockTestBase):
             d.refresh_from_db()
 
         # Initial checks
-        self.assertEqual(a.pathstring, "A")
-        self.assertEqual(b.pathstring, "A/B")
-        self.assertEqual(c.pathstring, "A/B/C")
-        self.assertEqual(d.pathstring, "A/B/C/D")
+        self.assertEqual(a.pathstring, 'A')
+        self.assertEqual(b.pathstring, 'A/B')
+        self.assertEqual(c.pathstring, 'A/B/C')
+        self.assertEqual(d.pathstring, 'A/B/C/D')
 
-        c.name = "Cc"
+        c.name = 'Cc'
         c.save()
 
         refresh()
-        self.assertEqual(a.pathstring, "A")
-        self.assertEqual(b.pathstring, "A/B")
-        self.assertEqual(c.pathstring, "A/B/Cc")
-        self.assertEqual(d.pathstring, "A/B/Cc/D")
+        self.assertEqual(a.pathstring, 'A')
+        self.assertEqual(b.pathstring, 'A/B')
+        self.assertEqual(c.pathstring, 'A/B/Cc')
+        self.assertEqual(d.pathstring, 'A/B/Cc/D')
 
-        b.name = "Bb"
+        b.name = 'Bb'
         b.save()
 
         refresh()
-        self.assertEqual(a.pathstring, "A")
-        self.assertEqual(b.pathstring, "A/Bb")
-        self.assertEqual(c.pathstring, "A/Bb/Cc")
-        self.assertEqual(d.pathstring, "A/Bb/Cc/D")
+        self.assertEqual(a.pathstring, 'A')
+        self.assertEqual(b.pathstring, 'A/Bb')
+        self.assertEqual(c.pathstring, 'A/Bb/Cc')
+        self.assertEqual(d.pathstring, 'A/Bb/Cc/D')
 
-        a.name = "Aa"
+        a.name = 'Aa'
         a.save()
 
         refresh()
-        self.assertEqual(a.pathstring, "Aa")
-        self.assertEqual(b.pathstring, "Aa/Bb")
-        self.assertEqual(c.pathstring, "Aa/Bb/Cc")
-        self.assertEqual(d.pathstring, "Aa/Bb/Cc/D")
+        self.assertEqual(a.pathstring, 'Aa')
+        self.assertEqual(b.pathstring, 'Aa/Bb')
+        self.assertEqual(c.pathstring, 'Aa/Bb/Cc')
+        self.assertEqual(d.pathstring, 'Aa/Bb/Cc/D')
 
-        d.name = "Dd"
+        d.name = 'Dd'
         d.save()
 
         refresh()
-        self.assertEqual(a.pathstring, "Aa")
-        self.assertEqual(b.pathstring, "Aa/Bb")
-        self.assertEqual(c.pathstring, "Aa/Bb/Cc")
-        self.assertEqual(d.pathstring, "Aa/Bb/Cc/Dd")
+        self.assertEqual(a.pathstring, 'Aa')
+        self.assertEqual(b.pathstring, 'Aa/Bb')
+        self.assertEqual(c.pathstring, 'Aa/Bb/Cc')
+        self.assertEqual(d.pathstring, 'Aa/Bb/Cc/Dd')
 
         # Test a really long name
         # (it will be clipped to < 250 characters)
-        a.name = "A" * 100
+        a.name = 'A' * 100
         a.save()
-        b.name = "B" * 100
+        b.name = 'B' * 100
         b.save()
-        c.name = "C" * 100
+        c.name = 'C' * 100
         c.save()
-        d.name = "D" * 100
+        d.name = 'D' * 100
         d.save()
 
         refresh()
@@ -124,8 +124,8 @@ class StockTest(StockTestBase):
         self.assertEqual(len(c.pathstring), 249)
         self.assertEqual(len(d.pathstring), 249)
 
-        self.assertTrue(d.pathstring.startswith("AAAAAAAA"))
-        self.assertTrue(d.pathstring.endswith("DDDDDDDD"))
+        self.assertTrue(d.pathstring.startswith('AAAAAAAA'))
+        self.assertTrue(d.pathstring.endswith('DDDDDDDD'))
 
     def test_link(self):
         """Test the link URL field validation"""
@@ -460,8 +460,8 @@ class StockTest(StockTestBase):
         it = StockItem.objects.get(pk=2)
         n = it.quantity
         an = n - 10
-        customer = Company.objects.create(name="MyTestCompany")
-        order = SalesOrder.objects.create(description="Test order")
+        customer = Company.objects.create(name='MyTestCompany')
+        order = SalesOrder.objects.create(description='Test order')
         ait = it.allocateToCustomer(
             customer, quantity=an, order=order, user=None, notes='Allocated some stock'
         )
@@ -491,18 +491,18 @@ class StockTest(StockTestBase):
 
         # First establish total stock for this part
         allstock_before = StockItem.objects.filter(part=it.part).aggregate(
-            Sum("quantity")
-        )["quantity__sum"]
+            Sum('quantity')
+        )['quantity__sum']
 
         n = it.quantity
         an = n - 10
-        customer = Company.objects.create(name="MyTestCompany")
-        order = SalesOrder.objects.create(description="Test order")
+        customer = Company.objects.create(name='MyTestCompany')
+        order = SalesOrder.objects.create(description='Test order')
 
         ait = it.allocateToCustomer(
             customer, quantity=an, order=order, user=None, notes='Allocated some stock'
         )
-        ait.return_from_customer(it.location, None, notes="Stock removed from customer")
+        ait.return_from_customer(it.location, None, notes='Stock removed from customer')
 
         # When returned stock is returned to its original (parent) location, check that the parent has correct quantity
         self.assertEqual(it.quantity, n)
@@ -511,7 +511,7 @@ class StockTest(StockTestBase):
             customer, quantity=an, order=order, user=None, notes='Allocated some stock'
         )
         ait.return_from_customer(
-            self.drawer3, None, notes="Stock removed from customer"
+            self.drawer3, None, notes='Stock removed from customer'
         )
 
         # Check correct assignment of the new location
@@ -533,8 +533,8 @@ class StockTest(StockTestBase):
 
         # Establish total stock for the part after remove from customer to check that we still have the correct quantity in stock
         allstock_after = StockItem.objects.filter(part=it.part).aggregate(
-            Sum("quantity")
-        )["quantity__sum"]
+            Sum('quantity')
+        )['quantity__sum']
         self.assertEqual(allstock_before, allstock_after)
 
     def test_take_stock(self):
@@ -621,7 +621,7 @@ class StockTest(StockTestBase):
 
             self.assertEqual(item.serial_int, 12345)
 
-        item.serial = "-123"
+        item.serial = '-123'
         item.save()
 
         # Negative number should map to positive value
@@ -685,14 +685,14 @@ class StockTest(StockTestBase):
 
         # Try an invalid quantity
         with self.assertRaises(ValidationError):
-            item.serializeStock("k", [], self.user)
+            item.serializeStock('k', [], self.user)
 
         with self.assertRaises(ValidationError):
             item.serializeStock(-1, [], self.user)
 
         # Not enough serial numbers for all stock items.
         with self.assertRaises(ValidationError):
-            item.serializeStock(3, "hello", self.user)
+            item.serializeStock(3, 'hello', self.user)
 
     def test_serialize_stock_valid(self):
         """Perform valid stock serializations."""
@@ -996,7 +996,7 @@ class TestResultTest(StockTestBase):
         tests = item.test_results
         self.assertEqual(tests.count(), 4)
 
-        results = item.getTestResults(test="Temperature Test")
+        results = item.getTestResults(test='Temperature Test')
         self.assertEqual(results.count(), 2)
 
         # Passing tests
@@ -1057,25 +1057,25 @@ class TestResultTest(StockTestBase):
         item.quantity = 50
 
         # Try with an invalid batch code (according to sample validatoin plugin)
-        item.batch = "X234"
+        item.batch = 'X234'
 
         with self.assertRaises(ValidationError):
             item.save()
 
-        item.batch = "B123"
+        item.batch = 'B123'
         item.save()
 
         # Do some tests!
         StockItemTestResult.objects.create(
-            stock_item=item, test="Firmware", result=True
+            stock_item=item, test='Firmware', result=True
         )
 
         StockItemTestResult.objects.create(
-            stock_item=item, test="Paint Color", result=True, value="Red"
+            stock_item=item, test='Paint Color', result=True, value='Red'
         )
 
         StockItemTestResult.objects.create(
-            stock_item=item, test="Applied Sticker", result=False
+            stock_item=item, test='Applied Sticker', result=False
         )
 
         self.assertEqual(item.test_results.count(), 3)
diff --git a/InvenTree/users/admin.py b/InvenTree/users/admin.py
index 67629b381e..f7758206a9 100644
--- a/InvenTree/users/admin.py
+++ b/InvenTree/users/admin.py
@@ -247,9 +247,9 @@ class RoleGroupAdmin(admin.ModelAdmin):  # pragma: no cover
         # If any, display warning message when group is saved
         if len(multiple_group_users) > 0:
             msg = (
-                _("The following users are members of multiple groups")
-                + ": "
-                + ", ".join(multiple_group_users)
+                _('The following users are members of multiple groups')
+                + ': '
+                + ', '.join(multiple_group_users)
             )
 
             messages.add_message(request, messages.WARNING, msg)
diff --git a/InvenTree/users/apps.py b/InvenTree/users/apps.py
index 0719369218..facdf2f15a 100644
--- a/InvenTree/users/apps.py
+++ b/InvenTree/users/apps.py
@@ -43,7 +43,7 @@ class UsersConfig(AppConfig):
             if (
                 rule.name not in RuleSet.RULESET_NAMES
             ):  # pragma: no cover  # can not change ORM without the app being loaded
-                logger.info("Deleting outdated ruleset: %s", rule.name)
+                logger.info('Deleting outdated ruleset: %s', rule.name)
                 rule.delete()
 
         # Update group permission assignments for all groups
diff --git a/InvenTree/users/authentication.py b/InvenTree/users/authentication.py
index 659fafcc48..aad1cfdd5b 100644
--- a/InvenTree/users/authentication.py
+++ b/InvenTree/users/authentication.py
@@ -26,10 +26,10 @@ class ApiTokenAuthentication(TokenAuthentication):
         (user, token) = super().authenticate_credentials(key)
 
         if token.revoked:
-            raise exceptions.AuthenticationFailed(_("Token has been revoked"))
+            raise exceptions.AuthenticationFailed(_('Token has been revoked'))
 
         if token.expired:
-            raise exceptions.AuthenticationFailed(_("Token has expired"))
+            raise exceptions.AuthenticationFailed(_('Token has expired'))
 
         if token.last_seen != datetime.date.today():
             # Update the last-seen date
diff --git a/InvenTree/users/models.py b/InvenTree/users/models.py
index 1d8753ef4d..8b40192915 100644
--- a/InvenTree/users/models.py
+++ b/InvenTree/users/models.py
@@ -26,7 +26,7 @@ import InvenTree.helpers
 import InvenTree.models
 from InvenTree.ready import canAppAccessDatabase
 
-logger = logging.getLogger("inventree")
+logger = logging.getLogger('inventree')
 
 
 #  OVERRIDE START
@@ -41,7 +41,7 @@ def user_model_str(self):
     return self.username
 
 
-User.add_to_class("__str__", user_model_str)  # Overriding User.__str__
+User.add_to_class('__str__', user_model_str)  # Overriding User.__str__
 #  OVERRIDE END
 
 
@@ -445,7 +445,7 @@ class RuleSet(models.Model):
         """Construct the correctly formatted permission string, given the app_model name, and the permission type."""
         model, app = split_model(model)
 
-        return f"{app}.{permission}_{model}"
+        return f'{app}.{permission}_{model}'
 
     def __str__(self, debug=False):  # pragma: no cover
         """Ruleset string representation."""
@@ -525,7 +525,7 @@ def update_group_roles(group, debug=False):
     # and create a simplified permission key string
     for p in group.permissions.all().prefetch_related('content_type'):
         (permission, app, model) = p.natural_key()
-        permission_string = f"{app}.{permission}"
+        permission_string = f'{app}.{permission}'
         group_permissions.add(permission_string)
 
     # List of permissions which must be added to the group
@@ -543,7 +543,7 @@ def update_group_roles(group, debug=False):
             allowed: Whether or not the action is allowed
         """
         if action not in ['view', 'add', 'change', 'delete']:  # pragma: no cover
-            raise ValueError(f"Action {action} is invalid")
+            raise ValueError(f'Action {action} is invalid')
 
         permission_string = RuleSet.get_model_permission_string(model, action)
 
@@ -624,7 +624,7 @@ def update_group_roles(group, debug=False):
             group.permissions.add(permission)
 
         if debug:  # pragma: no cover
-            logger.debug("Adding permission %s to group %s", perm, group.name)
+            logger.debug('Adding permission %s to group %s', perm, group.name)
 
     # Remove any extra permissions from the group
     for perm in permissions_to_delete:
@@ -638,7 +638,7 @@ def update_group_roles(group, debug=False):
             group.permissions.remove(permission)
 
         if debug:  # pragma: no cover
-            logger.debug("Removing permission %s from group %s", perm, group.name)
+            logger.debug('Removing permission %s from group %s', perm, group.name)
 
     # Enable all action permissions for certain children models
     # if parent model has 'change' permission
@@ -661,7 +661,7 @@ def update_group_roles(group, debug=False):
                     if permission:
                         group.permissions.add(permission)
                         logger.debug(
-                            "Adding permission %s to group %s", child_perm, group.name
+                            'Adding permission %s to group %s', child_perm, group.name
                         )
 
 
@@ -675,7 +675,7 @@ def clear_user_role_cache(user):
     """
     for role in RuleSet.RULESET_MODELS.keys():
         for perm in ['add', 'change', 'view', 'delete']:
-            key = f"role_{user}_{role}_{perm}"
+            key = f'role_{user}_{role}_{perm}'
             cache.delete(key)
 
 
@@ -707,7 +707,7 @@ def check_user_role(user, role, permission):
         return True
 
     # First, check the cache
-    key = f"role_{user}_{role}_{permission}"
+    key = f'role_{user}_{role}_{permission}'
 
     result = cache.get(key)
 
diff --git a/InvenTree/users/tests.py b/InvenTree/users/tests.py
index 21ff21805b..76eee1c0f6 100644
--- a/InvenTree/users/tests.py
+++ b/InvenTree/users/tests.py
@@ -21,27 +21,27 @@ class RuleSetModelTest(TestCase):
         missing = [name for name in RuleSet.RULESET_NAMES if name not in keys]
 
         if len(missing) > 0:  # pragma: no cover
-            print("The following rulesets do not have models assigned:")
+            print('The following rulesets do not have models assigned:')
             for m in missing:
-                print("-", m)
+                print('-', m)
 
         # Check if models have been defined for a ruleset which is incorrect
         extra = [name for name in keys if name not in RuleSet.RULESET_NAMES]
 
         if len(extra) > 0:  # pragma: no cover
             print(
-                "The following rulesets have been improperly added to RULESET_MODELS:"
+                'The following rulesets have been improperly added to RULESET_MODELS:'
             )
             for e in extra:
-                print("-", e)
+                print('-', e)
 
         # Check that each ruleset has models assigned
         empty = [key for key in keys if len(RuleSet.RULESET_MODELS[key]) == 0]
 
         if len(empty) > 0:  # pragma: no cover
-            print("The following rulesets have empty entries in RULESET_MODELS:")
+            print('The following rulesets have empty entries in RULESET_MODELS:')
             for e in empty:
-                print("-", e)
+                print('-', e)
 
         self.assertEqual(len(missing), 0)
         self.assertEqual(len(extra), 0)
@@ -78,10 +78,10 @@ class RuleSetModelTest(TestCase):
 
         if len(missing_models) > 0:  # pragma: no cover
             print(
-                "The following database models are not covered by the defined RuleSet permissions:"
+                'The following database models are not covered by the defined RuleSet permissions:'
             )
             for m in missing_models:
-                print("-", m)
+                print('-', m)
 
         extra_models = set()
 
@@ -98,9 +98,9 @@ class RuleSetModelTest(TestCase):
                 extra_models.add(model)
 
         if len(extra_models) > 0:  # pragma: no cover
-            print("The following RuleSet permissions do not match a database model:")
+            print('The following RuleSet permissions do not match a database model:')
             for m in extra_models:
-                print("-", m)
+                print('-', m)
 
         self.assertEqual(len(missing_models), 0)
         self.assertEqual(len(extra_models), 0)
@@ -108,7 +108,7 @@ class RuleSetModelTest(TestCase):
     def test_permission_assign(self):
         """Test that the permission assigning works!"""
         # Create a new group
-        group = Group.objects.create(name="Test group")
+        group = Group.objects.create(name='Test group')
 
         rulesets = group.rule_sets.all()
 
diff --git a/InvenTree/web/templatetags/spa_helper.py b/InvenTree/web/templatetags/spa_helper.py
index 9b2e09be45..003682402a 100644
--- a/InvenTree/web/templatetags/spa_helper.py
+++ b/InvenTree/web/templatetags/spa_helper.py
@@ -9,7 +9,7 @@ from django import template
 from django.conf import settings
 from django.utils.safestring import mark_safe
 
-logger = getLogger("InvenTree")
+logger = getLogger('InvenTree')
 register = template.Library()
 
 FRONTEND_SETTINGS = json.dumps(settings.FRONTEND_SETTINGS)
@@ -21,36 +21,36 @@ def spa_bundle(manifest_path: Union[str, Path] = '', app: str = 'web'):
 
     def get_url(file: str) -> str:
         """Get static url for file."""
-        return f"{settings.STATIC_URL}{app}/{file}"
+        return f'{settings.STATIC_URL}{app}/{file}'
 
     if manifest_path == '':
         manifest_path = Path(__file__).parent.parent.joinpath(
-            "static/web/manifest.json"
+            'static/web/manifest.json'
         )
     manifest = Path(manifest_path)
 
     if not manifest.exists():
-        logger.error("Manifest file not found")
+        logger.error('Manifest file not found')
         return
 
     try:
         manifest_data = json.load(manifest.open())
     except (TypeError, json.decoder.JSONDecodeError):
-        logger.exception("Failed to parse manifest file")
+        logger.exception('Failed to parse manifest file')
         return
 
-    return_string = ""
+    return_string = ''
     # CSS (based on index.css file as entrypoint)
-    css_index = manifest_data.get("index.css")
+    css_index = manifest_data.get('index.css')
     if css_index:
         return_string += (
             f'<link rel="stylesheet" href="{get_url(css_index["file"])}" />'
         )
 
     # JS (based on index.html file as entrypoint)
-    index = manifest_data.get("index.html")
-    dynamic_files = index.get("dynamicImports", [])
-    imports_files = "".join([
+    index = manifest_data.get('index.html')
+    dynamic_files = index.get('dynamicImports', [])
+    imports_files = ''.join([
         f'<script type="module" src="{get_url(manifest_data[file]["file"])}"></script>'
         for file in dynamic_files
     ])
diff --git a/InvenTree/web/tests.py b/InvenTree/web/tests.py
index 0419741564..1975bfd3f2 100644
--- a/InvenTree/web/tests.py
+++ b/InvenTree/web/tests.py
@@ -31,7 +31,7 @@ class TemplateTagTest(InvenTreeTestCase):
         self.assertTrue(len(shipped_js) > 0)
         self.assertTrue(len(shipped_js) == 3)
 
-        manifest_file = Path(__file__).parent.joinpath("static/web/manifest.json")
+        manifest_file = Path(__file__).parent.joinpath('static/web/manifest.json')
         # Try with removed manifest file
         manifest_file.rename(manifest_file.with_suffix('.json.bak'))  # Rename
         resp = resp = spa_helper.spa_bundle()
diff --git a/InvenTree/web/urls.py b/InvenTree/web/urls.py
index 02d24a8033..16cdb57f40 100644
--- a/InvenTree/web/urls.py
+++ b/InvenTree/web/urls.py
@@ -17,23 +17,23 @@ class RedirectAssetView(TemplateView):
         )
 
 
-spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name="web/index.html"))
-assets_path = path("assets/<path:path>", RedirectAssetView.as_view())
+spa_view = ensure_csrf_cookie(TemplateView.as_view(template_name='web/index.html'))
+assets_path = path('assets/<path:path>', RedirectAssetView.as_view())
 
 
 urlpatterns = [
     path(
-        f"{settings.FRONTEND_URL_BASE}/",
+        f'{settings.FRONTEND_URL_BASE}/',
         include([
             assets_path,
             path(
-                "set-password?uid=<uid>&token=<token>",
+                'set-password?uid=<uid>&token=<token>',
                 spa_view,
-                name="password_reset_confirm",
+                name='password_reset_confirm',
             ),
-            re_path(".*", spa_view),
+            re_path('.*', spa_view),
         ]),
     ),
     assets_path,
-    path(settings.FRONTEND_URL_BASE, spa_view, name="platform"),
+    path(settings.FRONTEND_URL_BASE, spa_view, name='platform'),
 ]
diff --git a/ci/check_api_endpoint.py b/ci/check_api_endpoint.py
index ef263ee9f1..b216fce32f 100644
--- a/ci/check_api_endpoint.py
+++ b/ci/check_api_endpoint.py
@@ -5,15 +5,15 @@ import json
 import requests
 
 # We expect the server to be running on the local host
-url = "http://localhost:8000/api/"
+url = 'http://localhost:8000/api/'
 
-print("Testing InvenTree API endpoint")
+print('Testing InvenTree API endpoint')
 
 response = requests.get(url)
 
 assert response.status_code == 200
 
-print("- Response 200 OK")
+print('- Response 200 OK')
 
 data = json.loads(response.text)
 
@@ -26,6 +26,6 @@ for key in required_keys:
 # Check that the worker is running
 assert data['worker_running']
 
-print("- Background worker is operational")
+print('- Background worker is operational')
 
-print("API Endpoint Tests Passed OK")
+print('API Endpoint Tests Passed OK')
diff --git a/ci/check_js_templates.py b/ci/check_js_templates.py
index f51a89a243..3c3cee8e48 100644
--- a/ci/check_js_templates.py
+++ b/ci/check_js_templates.py
@@ -18,14 +18,14 @@ js_dynamic_dir = os.path.join(template_dir, 'js', 'dynamic')
 
 errors = 0
 
-print("=================================")
-print("Checking static javascript files:")
-print("=================================")
+print('=================================')
+print('Checking static javascript files:')
+print('=================================')
 
 
 def check_invalid_tag(data):
     """Check for invalid tags."""
-    pattern = r"{%(\w+)"
+    pattern = r'{%(\w+)'
 
     err_count = 0
 
@@ -35,7 +35,7 @@ def check_invalid_tag(data):
         for result in results:
             err_count += 1
 
-            print(f" - Error on line {idx+1}: %{{{result[0]}")
+            print(f' - Error on line {idx+1}: %{{{result[0]}')
 
     return err_count
 
@@ -55,7 +55,7 @@ def check_prohibited_tags(data):
         'url',
     ]
 
-    pattern = r"{% (\w+)\s"
+    pattern = r'{% (\w+)\s'
 
     err_count = 0
 
@@ -94,9 +94,9 @@ for filename in pathlib.Path(js_dynamic_dir).rglob('*.js'):
         if len(results) > 0:
             errors += 1
 
-            print(f" > prohibited {{% trans %}} tag found at line {idx + 1}")
+            print(f' > prohibited {{% trans %}} tag found at line {idx + 1}')
 
 if errors > 0:
-    print(f"Found {errors} incorrect template tags")
+    print(f'Found {errors} incorrect template tags')
 
 sys.exit(errors)
diff --git a/ci/check_locale_files.py b/ci/check_locale_files.py
index 3bfbd66943..d5e2b89fbe 100644
--- a/ci/check_locale_files.py
+++ b/ci/check_locale_files.py
@@ -3,7 +3,7 @@
 import subprocess
 import sys
 
-print("Checking for uncommitted locale files...")
+print('Checking for uncommitted locale files...')
 
 cmd = ['git', 'status']
 
@@ -19,9 +19,9 @@ for line in str(out.decode()).split('\n'):
         locales.append(line)
 
 if len(locales) > 0:
-    print("There are {n} unstaged locale files:".format(n=len(locales)))
+    print('There are {n} unstaged locale files:'.format(n=len(locales)))
 
     for lang in locales:
-        print(" - {l}".format(l=lang))
+        print(' - {l}'.format(l=lang))
 
 sys.exit(len(locales))
diff --git a/ci/check_migration_files.py b/ci/check_migration_files.py
index 24668350be..d224848f02 100644
--- a/ci/check_migration_files.py
+++ b/ci/check_migration_files.py
@@ -3,7 +3,7 @@
 import subprocess
 import sys
 
-print("Checking for unstaged migration files...")
+print('Checking for unstaged migration files...')
 
 cmd = ['git', 'ls-files', '--exclude-standard', '--others']
 
@@ -20,9 +20,9 @@ for line in str(out.decode()).split('\n'):
 if len(migrations) == 0:
     sys.exit(0)
 
-print("There are {n} unstaged migration files:".format(n=len(migrations)))
+print('There are {n} unstaged migration files:'.format(n=len(migrations)))
 
 for m in migrations:
-    print(" - {m}".format(m=m))
+    print(' - {m}'.format(m=m))
 
 sys.exit(len(migrations))
diff --git a/ci/version_check.py b/ci/version_check.py
index 39aa52bbe0..2c18da0880 100644
--- a/ci/version_check.py
+++ b/ci/version_check.py
@@ -26,7 +26,7 @@ def get_existing_release_tags():
     headers = None
 
     if token:
-        headers = {"Authorization": f"Bearer {token}"}
+        headers = {'Authorization': f'Bearer {token}'}
 
     response = requests.get(
         'https://api.github.com/repos/inventree/inventree/releases', headers=headers
@@ -44,7 +44,7 @@ def get_existing_release_tags():
 
     for release in data:
         tag = release['tag_name'].strip()
-        match = re.match(r"^.*(\d+)\.(\d+)\.(\d+).*$", tag)
+        match = re.match(r'^.*(\d+)\.(\d+)\.(\d+).*$', tag)
 
         if len(match.groups()) != 3:
             print(f"Version '{tag}' did not match expected pattern")
@@ -64,7 +64,7 @@ def check_version_number(version_string, allow_duplicate=False):
     print(f"Checking version '{version_string}'")
 
     # Check that the version string matches the required format
-    match = re.match(r"^(\d+)\.(\d+)\.(\d+)(?: dev)?$", version_string)
+    match = re.match(r'^(\d+)\.(\d+)\.(\d+)(?: dev)?$', version_string)
 
     if not match or len(match.groups()) != 3:
         raise ValueError(
@@ -85,7 +85,7 @@ def check_version_number(version_string, allow_duplicate=False):
 
         if release > version_tuple:
             highest_release = False
-            print(f"Found newer release: {str(release)}")
+            print(f'Found newer release: {str(release)}')
 
     return highest_release
 
@@ -104,10 +104,10 @@ if __name__ == '__main__':
     GITHUB_BASE_REF = os.environ['GITHUB_BASE_REF']
 
     # Print out version information, makes debugging actions *much* easier!
-    print(f"GITHUB_REF: {GITHUB_REF}")
-    print(f"GITHUB_REF_NAME: {GITHUB_REF_NAME}")
-    print(f"GITHUB_REF_TYPE: {GITHUB_REF_TYPE}")
-    print(f"GITHUB_BASE_REF: {GITHUB_BASE_REF}")
+    print(f'GITHUB_REF: {GITHUB_REF}')
+    print(f'GITHUB_REF_NAME: {GITHUB_REF_NAME}')
+    print(f'GITHUB_REF_TYPE: {GITHUB_REF_TYPE}')
+    print(f'GITHUB_BASE_REF: {GITHUB_BASE_REF}')
 
     version_file = os.path.join(here, '..', 'InvenTree', 'InvenTree', 'version.py')
 
@@ -120,7 +120,7 @@ if __name__ == '__main__':
         results = re.findall(r'INVENTREE_SW_VERSION = "(.*)"', text)
 
         if len(results) != 1:
-            print(f"Could not find INVENTREE_SW_VERSION in {version_file}")
+            print(f'Could not find INVENTREE_SW_VERSION in {version_file}')
             sys.exit(1)
 
         version = results[0]
@@ -164,15 +164,15 @@ if __name__ == '__main__':
         docker_tags = ['latest']
 
     else:
-        print("Unsupported branch / version combination:")
-        print(f"InvenTree Version: {version}")
-        print("GITHUB_REF_TYPE:", GITHUB_REF_TYPE)
-        print("GITHUB_BASE_REF:", GITHUB_BASE_REF)
-        print("GITHUB_REF:", GITHUB_REF)
+        print('Unsupported branch / version combination:')
+        print(f'InvenTree Version: {version}')
+        print('GITHUB_REF_TYPE:', GITHUB_REF_TYPE)
+        print('GITHUB_BASE_REF:', GITHUB_BASE_REF)
+        print('GITHUB_REF:', GITHUB_REF)
         sys.exit(1)
 
     if docker_tags is None:
-        print("Docker tags could not be determined")
+        print('Docker tags could not be determined')
         sys.exit(1)
 
     print(f"Version check passed for '{version}'!")
@@ -181,9 +181,9 @@ if __name__ == '__main__':
     # Ref: https://getridbug.com/python/how-to-set-environment-variables-in-github-actions-using-python/
     with open(os.getenv('GITHUB_ENV'), 'a') as env_file:
         # Construct tag string
-        tags = ",".join([f"inventree/inventree:{tag}" for tag in docker_tags])
+        tags = ','.join([f'inventree/inventree:{tag}' for tag in docker_tags])
 
-        env_file.write(f"docker_tags={tags}\n")
+        env_file.write(f'docker_tags={tags}\n')
 
         if GITHUB_REF_TYPE == 'tag' and highest_release:
-            env_file.write("stable_release=true\n")
+            env_file.write('stable_release=true\n')
diff --git a/docker/gunicorn.conf.py b/docker/gunicorn.conf.py
index 256bd5c0b7..6d36a74269 100644
--- a/docker/gunicorn.conf.py
+++ b/docker/gunicorn.conf.py
@@ -33,7 +33,7 @@ if workers is not None:
 if workers is None:
     workers = multiprocessing.cpu_count() * 2 + 1
 
-logger.info("Starting gunicorn server with %s workers", workers)
+logger.info('Starting gunicorn server with %s workers', workers)
 
 max_requests = 1000
 max_requests_jitter = 50
diff --git a/docs/docs/hooks.py b/docs/docs/hooks.py
index 9e4a5c4d97..6b33829c20 100644
--- a/docs/docs/hooks.py
+++ b/docs/docs/hooks.py
@@ -11,7 +11,7 @@ import requests
 
 def fetch_rtd_versions():
     """Get a list of RTD docs versions to build the version selector"""
-    print("Fetching documentation versions from ReadTheDocs")
+    print('Fetching documentation versions from ReadTheDocs')
 
     versions = []
 
@@ -20,7 +20,7 @@ def fetch_rtd_versions():
         response = requests.get(url, headers=headers)
 
         if response.status_code != 200:
-            print(f"Error fetching RTD versions: {response.status_code}")
+            print(f'Error fetching RTD versions: {response.status_code}')
             return
 
         data = json.loads(response.text)
@@ -48,10 +48,10 @@ def fetch_rtd_versions():
     token = os.environ.get('RTD_TOKEN', None)
     if token:
         headers = {'Authorization': f'Token {token}'}
-        url = "https://readthedocs.org/api/v3/projects/inventree/versions/?active=true&limit=50"
+        url = 'https://readthedocs.org/api/v3/projects/inventree/versions/?active=true&limit=50'
         make_request(url, headers)
     else:
-        print("No RTD token found - skipping RTD version fetch")
+        print('No RTD token found - skipping RTD version fetch')
 
     # Sort versions by version number
     versions = sorted(versions, key=lambda x: StrictVersion(x['version']), reverse=True)
@@ -79,7 +79,7 @@ def fetch_rtd_versions():
 
     output_filename = os.path.join(os.path.dirname(__file__), 'versions.json')
 
-    print("Discovered the following versions:")
+    print('Discovered the following versions:')
     print(versions)
 
     with open(output_filename, 'w') as file:
@@ -104,14 +104,14 @@ def get_release_data():
             return json.loads(f.read())
 
     # Download release information via the GitHub API
-    print("Fetching InvenTree release information from api.github.com:")
+    print('Fetching InvenTree release information from api.github.com:')
     releases = []
 
     # Keep making API requests until we run out of results
     page = 1
 
     while 1:
-        url = f"https://api.github.com/repos/inventree/inventree/releases?page={page}&per_page=150"
+        url = f'https://api.github.com/repos/inventree/inventree/releases?page={page}&per_page=150'
 
         response = requests.get(url, timeout=30)
         assert response.status_code == 200
@@ -163,12 +163,12 @@ def on_config(config, *args, **kwargs):
         rtd_version = os.environ['READTHEDOCS_VERSION']
         rtd_language = os.environ['READTHEDOCS_LANGUAGE']
 
-        site_url = f"https://docs.inventree.org/{rtd_language}/{rtd_version}"
-        assets_dir = f"/{rtd_language}/{rtd_version}/assets"
+        site_url = f'https://docs.inventree.org/{rtd_language}/{rtd_version}'
+        assets_dir = f'/{rtd_language}/{rtd_version}/assets'
 
-        print("Building within READTHEDOCS environment!")
-        print(f" - Version: {rtd_version}")
-        print(f" - Language: {rtd_language}")
+        print('Building within READTHEDOCS environment!')
+        print(f' - Version: {rtd_version}')
+        print(f' - Language: {rtd_language}')
 
         # Add *all* readthedocs related keys
         readthedocs = {}
@@ -187,7 +187,7 @@ def on_config(config, *args, **kwargs):
 
     else:
         print("'READTHEDOCS' environment variable not found")
-        print("Building for localhost configuration!")
+        print('Building for localhost configuration!')
 
         assets_dir = '/assets'
         site_url = config['site_url']
@@ -215,7 +215,7 @@ def on_config(config, *args, **kwargs):
         re.match(r'^\d+\.\d+\.\d+$', tag)
 
         if not re.match:
-            print(f"Found badly formatted release: {tag}")
+            print(f'Found badly formatted release: {tag}')
             continue
 
         # Check if there is a local file with release information
@@ -238,7 +238,7 @@ def on_config(config, *args, **kwargs):
 
         releases.append(item)
 
-    print(f"- found {len(releases)} releases.")
+    print(f'- found {len(releases)} releases.')
 
     # Sort releases by descending date
     config['releases'] = sorted(releases, key=lambda it: it['date'], reverse=True)
diff --git a/pyproject.toml b/pyproject.toml
index f3807470d3..7bc74bf2b9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -69,7 +69,7 @@ known-first-party = ["src", "plugin", "InvenTree", "common"]
 "django" = ["django"]
 
 [tool.ruff.format]
-quote-style = "preserve"
+quote-style = "single"
 indent-style = "space"
 skip-magic-trailing-comma = true
 line-ending = "auto"
diff --git a/tasks.py b/tasks.py
index 63ad43ee04..b964397aea 100644
--- a/tasks.py
+++ b/tasks.py
@@ -22,7 +22,7 @@ def checkPythonVersion():
     REQ_MAJOR = 3
     REQ_MINOR = 9
 
-    version = sys.version.split(" ")[0]
+    version = sys.version.split(' ')[0]
 
     valid = True
 
@@ -33,8 +33,8 @@ def checkPythonVersion():
         valid = False
 
     if not valid:
-        print(f"The installed python version ({version}) is not supported!")
-        print(f"InvenTree requires Python {REQ_MAJOR}.{REQ_MINOR} or above")
+        print(f'The installed python version ({version}) is not supported!')
+        print(f'InvenTree requires Python {REQ_MAJOR}.{REQ_MINOR} or above')
         sys.exit(1)
 
 
@@ -62,26 +62,26 @@ def apps():
 def content_excludes():
     """Returns a list of content types to exclude from import/export."""
     excludes = [
-        "contenttypes",
-        "auth.permission",
-        "users.apitoken",
-        "error_report.error",
-        "admin.logentry",
-        "django_q.schedule",
-        "django_q.task",
-        "django_q.ormq",
-        "users.owner",
-        "exchange.rate",
-        "exchange.exchangebackend",
-        "common.notificationentry",
-        "common.notificationmessage",
-        "user_sessions.session",
+        'contenttypes',
+        'auth.permission',
+        'users.apitoken',
+        'error_report.error',
+        'admin.logentry',
+        'django_q.schedule',
+        'django_q.task',
+        'django_q.ormq',
+        'users.owner',
+        'exchange.rate',
+        'exchange.exchangebackend',
+        'common.notificationentry',
+        'common.notificationmessage',
+        'user_sessions.session',
     ]
 
-    output = ""
+    output = ''
 
     for e in excludes:
-        output += f"--exclude {e} "
+        output += f'--exclude {e} '
 
     return output
 
@@ -175,12 +175,12 @@ def check_file_existance(filename: str, overwrite: bool = False):
     """
     if Path(filename).is_file() and overwrite is False:
         response = input(
-            "Warning: file already exists. Do you want to overwrite? [y/N]: "
+            'Warning: file already exists. Do you want to overwrite? [y/N]: '
         )
         response = str(response).strip().lower()
 
         if response not in ['y', 'yes']:
-            print("Cancelled export operation")
+            print('Cancelled export operation')
             sys.exit(1)
 
 
@@ -220,12 +220,12 @@ def setup_dev(c, tests=False):
     c.run('pip3 install -U -r requirements-dev.txt')
 
     # Install pre-commit hook
-    print("Installing pre-commit for checks before git commits...")
+    print('Installing pre-commit for checks before git commits...')
     c.run('pre-commit install')
 
     # Update all the hooks
     c.run('pre-commit autoupdate')
-    print("pre-commit set up is done...")
+    print('pre-commit set up is done...')
 
     # Set up test-data if flag is set
     if tests:
@@ -242,19 +242,19 @@ def superuser(c):
 @task
 def rebuild_models(c):
     """Rebuild database models with MPTT structures."""
-    manage(c, "rebuild_models", pty=True)
+    manage(c, 'rebuild_models', pty=True)
 
 
 @task
 def rebuild_thumbnails(c):
     """Rebuild missing image thumbnails."""
-    manage(c, "rebuild_thumbnails", pty=True)
+    manage(c, 'rebuild_thumbnails', pty=True)
 
 
 @task
 def clean_settings(c):
     """Clean the setting tables of old settings."""
-    manage(c, "clean_settings")
+    manage(c, 'clean_settings')
 
 
 @task(help={'mail': "mail of the user who's MFA should be disabled"})
@@ -263,16 +263,16 @@ def remove_mfa(c, mail=''):
     if not mail:
         print('You must provide a users mail')
 
-    manage(c, f"remove_mfa {mail}")
+    manage(c, f'remove_mfa {mail}')
 
 
 @task(help={'frontend': 'Build the frontend'})
 def static(c, frontend=False):
     """Copies required static files to the STATIC_ROOT directory, as per Django requirements."""
-    manage(c, "prerender")
+    manage(c, 'prerender')
     if frontend and node_available():
         frontend_build(c)
-    manage(c, "collectstatic --no-input")
+    manage(c, 'collectstatic --no-input')
 
 
 @task
@@ -286,7 +286,7 @@ def translate_stats(c):
     try:
         manage(c, 'compilemessages', pty=True)
     except Exception:
-        print("WARNING: Translation files could not be compiled:")
+        print('WARNING: Translation files could not be compiled:')
 
     path = Path('InvenTree', 'script', 'translation_stats.py')
     c.run(f'python3 {path}')
@@ -300,8 +300,8 @@ def translate(c):
     it is performed as part of the InvenTree translation toolchain.
     """
     # Translate applicable .py / .html / .js / .tsx files
-    manage(c, "makemessages --all -e py,html,js --no-wrap")
-    manage(c, "compilemessages")
+    manage(c, 'makemessages --all -e py,html,js --no-wrap')
+    manage(c, 'compilemessages')
 
     if node_available():
         frontend_install(c)
@@ -315,19 +315,19 @@ def translate(c):
 @task
 def backup(c):
     """Backup the database and media files."""
-    print("Backing up InvenTree database...")
-    manage(c, "dbbackup --noinput --clean --compress")
-    print("Backing up InvenTree media files...")
-    manage(c, "mediabackup --noinput --clean --compress")
+    print('Backing up InvenTree database...')
+    manage(c, 'dbbackup --noinput --clean --compress')
+    print('Backing up InvenTree media files...')
+    manage(c, 'mediabackup --noinput --clean --compress')
 
 
 @task
 def restore(c):
     """Restore the database and media files."""
-    print("Restoring InvenTree database...")
-    manage(c, "dbrestore --noinput --uncompress")
-    print("Restoring InvenTree media files...")
-    manage(c, "mediarestore --noinput --uncompress")
+    print('Restoring InvenTree database...')
+    manage(c, 'dbrestore --noinput --uncompress')
+    print('Restoring InvenTree media files...')
+    manage(c, 'mediarestore --noinput --uncompress')
 
 
 @task(post=[rebuild_models, rebuild_thumbnails])
@@ -336,16 +336,16 @@ def migrate(c):
 
     This is a critical step if the database schema have been altered!
     """
-    print("Running InvenTree database migrations...")
-    print("========================================")
+    print('Running InvenTree database migrations...')
+    print('========================================')
 
-    manage(c, "makemigrations")
-    manage(c, "migrate --noinput")
-    manage(c, "migrate --run-syncdb")
-    manage(c, "check")
+    manage(c, 'makemigrations')
+    manage(c, 'migrate --noinput')
+    manage(c, 'migrate --run-syncdb')
+    manage(c, 'check')
 
-    print("========================================")
-    print("InvenTree database migrations completed!")
+    print('========================================')
+    print('InvenTree database migrations completed!')
 
 
 @task(
@@ -399,9 +399,9 @@ def update(c, skip_backup=False, frontend: bool = False):
 @task(
     help={
         'filename': "Output filename (default = 'data.json')",
-        'overwrite': "Overwrite existing files without asking first (default = off/False)",
-        'include_permissions': "Include user and group permissions in the output file (filename) (default = off/False)",
-        'delete_temp': "Delete temporary files (containing permissions) at end of run. Note that this will delete temporary files from previous runs as well. (default = off/False)",
+        'overwrite': 'Overwrite existing files without asking first (default = off/False)',
+        'include_permissions': 'Include user and group permissions in the output file (filename) (default = off/False)',
+        'delete_temp': 'Delete temporary files (containing permissions) at end of run. Note that this will delete temporary files from previous runs as well. (default = off/False)',
     }
 )
 def export_records(
@@ -436,38 +436,38 @@ def export_records(
 
     check_file_existance(filename, overwrite)
 
-    tmpfile = f"{filename}.tmp"
+    tmpfile = f'{filename}.tmp'
 
     cmd = f"dumpdata --indent 2 --output '{tmpfile}' {content_excludes()}"
 
     # Dump data to temporary file
     manage(c, cmd, pty=True)
 
-    print("Running data post-processing step...")
+    print('Running data post-processing step...')
 
     # Post-process the file, to remove any "permissions" specified for a user or group
-    with open(tmpfile, "r") as f_in:
+    with open(tmpfile, 'r') as f_in:
         data = json.loads(f_in.read())
 
     if include_permissions is False:
         for entry in data:
-            if "model" in entry:
+            if 'model' in entry:
                 # Clear out any permissions specified for a group
-                if entry["model"] == "auth.group":
-                    entry["fields"]["permissions"] = []
+                if entry['model'] == 'auth.group':
+                    entry['fields']['permissions'] = []
 
                 # Clear out any permissions specified for a user
-                if entry["model"] == "auth.user":
-                    entry["fields"]["user_permissions"] = []
+                if entry['model'] == 'auth.user':
+                    entry['fields']['user_permissions'] = []
 
     # Write the processed data to file
-    with open(filename, "w") as f_out:
+    with open(filename, 'w') as f_out:
         f_out.write(json.dumps(data, indent=2))
 
-    print("Data export completed")
+    print('Data export completed')
 
     if delete_temp is True:
-        print("Removing temporary file")
+        print('Removing temporary file')
         os.remove(tmpfile)
 
 
@@ -491,30 +491,30 @@ def import_records(c, filename='data.json', clear=False):
     print(f"Importing database records from '{filename}'")
 
     # Pre-process the data, to remove any "permissions" specified for a user or group
-    tmpfile = f"{filename}.tmp.json"
+    tmpfile = f'{filename}.tmp.json'
 
-    with open(filename, "r") as f_in:
+    with open(filename, 'r') as f_in:
         data = json.loads(f_in.read())
 
     for entry in data:
-        if "model" in entry:
+        if 'model' in entry:
             # Clear out any permissions specified for a group
-            if entry["model"] == "auth.group":
-                entry["fields"]["permissions"] = []
+            if entry['model'] == 'auth.group':
+                entry['fields']['permissions'] = []
 
             # Clear out any permissions specified for a user
-            if entry["model"] == "auth.user":
-                entry["fields"]["user_permissions"] = []
+            if entry['model'] == 'auth.user':
+                entry['fields']['user_permissions'] = []
 
     # Write the processed data to the tmp file
-    with open(tmpfile, "w") as f_out:
+    with open(tmpfile, 'w') as f_out:
         f_out.write(json.dumps(data, indent=2))
 
     cmd = f"loaddata '{tmpfile}' -i {content_excludes()}"
 
     manage(c, cmd, pty=True)
 
-    print("Data import completed")
+    print('Data import completed')
 
 
 @task
@@ -523,7 +523,7 @@ def delete_data(c, force=False):
 
     Warning: This will REALLY delete all records in the database!!
     """
-    print("Deleting all data from InvenTree database...")
+    print('Deleting all data from InvenTree database...')
 
     if force:
         manage(c, 'flush --noinput')
@@ -576,16 +576,16 @@ def import_fixtures(c):
 @task
 def wait(c):
     """Wait until the database connection is ready."""
-    return manage(c, "wait_for_db")
+    return manage(c, 'wait_for_db')
 
 
 @task(pre=[wait], help={'address': 'Server address:port (default=127.0.0.1:8000)'})
-def server(c, address="127.0.0.1:8000"):
+def server(c, address='127.0.0.1:8000'):
     """Launch a (development) server using Django's in-built webserver.
 
     Note: This is *not* sufficient for a production installation.
     """
-    manage(c, "runserver {address}".format(address=address), pty=True)
+    manage(c, 'runserver {address}'.format(address=address), pty=True)
 
 
 @task(pre=[wait])
@@ -598,7 +598,7 @@ def worker(c):
 @task
 def render_js_files(c):
     """Render templated javascript files (used for static testing)."""
-    manage(c, "test InvenTree.ci_render_js")
+    manage(c, 'test InvenTree.ci_render_js')
 
 
 @task(post=[translate_stats, static, server])
@@ -616,34 +616,34 @@ def test_translations(c):
     django.setup()
 
     # Add language
-    print("Add dummy language...")
-    print("========================================")
-    manage(c, "makemessages -e py,html,js --no-wrap -l xx")
+    print('Add dummy language...')
+    print('========================================')
+    manage(c, 'makemessages -e py,html,js --no-wrap -l xx')
 
     # change translation
-    print("Fill in dummy translations...")
-    print("========================================")
+    print('Fill in dummy translations...')
+    print('========================================')
 
     file_path = pathlib.Path(settings.LOCALE_PATHS[0], 'xx', 'LC_MESSAGES', 'django.po')
     new_file_path = str(file_path) + '_new'
 
     # compile regex
     reg = re.compile(
-        r"[a-zA-Z0-9]{1}"  # match any single letter and number  # noqa: W504
-        + r"(?![^{\(\<]*[}\)\>])"  # that is not inside curly brackets, brackets or a tag  # noqa: W504
-        + r"(?<![^\%][^\(][)][a-z])"  # that is not a specially formatted variable with singles  # noqa: W504
-        + r"(?![^\\][\n])"  # that is not a newline
+        r'[a-zA-Z0-9]{1}'  # match any single letter and number  # noqa: W504
+        + r'(?![^{\(\<]*[}\)\>])'  # that is not inside curly brackets, brackets or a tag  # noqa: W504
+        + r'(?<![^\%][^\(][)][a-z])'  # that is not a specially formatted variable with singles  # noqa: W504
+        + r'(?![^\\][\n])'  # that is not a newline
     )
     last_string = ''
 
     # loop through input file lines
-    with open(file_path, "rt") as file_org:
-        with open(new_file_path, "wt") as file_new:
+    with open(file_path, 'rt') as file_org:
+        with open(new_file_path, 'wt') as file_new:
             for line in file_org:
                 if line.startswith('msgstr "'):
                     # write output -> replace regex matches with x in the read in (multi)string
                     file_new.write(f'msgstr "{reg.sub("x", last_string[7:-2])}"\n')
-                    last_string = ""  # reset (multi)string
+                    last_string = ''  # reset (multi)string
                 elif line.startswith('msgid "'):
                     last_string = (
                         last_string + line
@@ -661,9 +661,9 @@ def test_translations(c):
     new_file_path.rename(file_path)
 
     # compile languages
-    print("Compile languages ...")
-    print("========================================")
-    manage(c, "compilemessages")
+    print('Compile languages ...')
+    print('========================================')
+    manage(c, 'compilemessages')
 
     # reset cwd
     os.chdir(base_path)
@@ -728,7 +728,7 @@ def test(
 
 
 @task(help={'dev': 'Set up development environment at the end'})
-def setup_test(c, ignore_update=False, dev=False, path="inventree-demo-dataset"):
+def setup_test(c, ignore_update=False, dev=False, path='inventree-demo-dataset'):
     """Setup a testing environment."""
     from InvenTree.InvenTree.config import get_media_dir
 
@@ -737,31 +737,31 @@ def setup_test(c, ignore_update=False, dev=False, path="inventree-demo-dataset")
 
     # Remove old data directory
     if os.path.exists(path):
-        print("Removing old data ...")
+        print('Removing old data ...')
         c.run(f'rm {path} -r')
 
     # Get test data
-    print("Cloning demo dataset ...")
+    print('Cloning demo dataset ...')
     c.run(f'git clone https://github.com/inventree/demo-dataset {path} -v --depth=1')
-    print("========================================")
+    print('========================================')
 
     # Make sure migrations are done - might have just deleted sqlite database
     if not ignore_update:
         migrate(c)
 
     # Load data
-    print("Loading database records ...")
+    print('Loading database records ...')
     import_records(c, filename=f'{path}/inventree_data.json', clear=True)
 
     # Copy media files
-    print("Copying media files ...")
+    print('Copying media files ...')
     src = Path(path).joinpath('media').resolve()
     dst = get_media_dir()
 
     shutil.copytree(src, dst, dirs_exist_ok=True)
 
-    print("Done setting up test environment...")
-    print("========================================")
+    print('Done setting up test environment...')
+    print('========================================')
 
     # Set up development setup if flag is set
     if dev:
@@ -771,7 +771,7 @@ def setup_test(c, ignore_update=False, dev=False, path="inventree-demo-dataset")
 @task(
     help={
         'filename': "Output filename (default = 'schema.yml')",
-        'overwrite': "Overwrite existing files without asking first (default = off/False)",
+        'overwrite': 'Overwrite existing files without asking first (default = off/False)',
     }
 )
 def schema(c, filename='schema.yml', overwrite=False):
@@ -850,8 +850,8 @@ def frontend_install(c):
     Args:
         c: Context variable
     """
-    print("Installing frontend dependencies")
-    yarn(c, "yarn install")
+    print('Installing frontend dependencies')
+    yarn(c, 'yarn install')
 
 
 @task
@@ -861,9 +861,9 @@ def frontend_trans(c):
     Args:
         c: Context variable
     """
-    print("Compiling frontend translations")
-    yarn(c, "yarn run extract")
-    yarn(c, "yarn run compile")
+    print('Compiling frontend translations')
+    yarn(c, 'yarn run extract')
+    yarn(c, 'yarn run compile')
 
 
 @task
@@ -873,8 +873,8 @@ def frontend_build(c):
     Args:
         c: Context variable
     """
-    print("Building frontend")
-    yarn(c, "yarn run build --emptyOutDir")
+    print('Building frontend')
+    yarn(c, 'yarn run build --emptyOutDir')
 
 
 @task
@@ -884,18 +884,18 @@ def frontend_dev(c):
     Args:
         c: Context variable
     """
-    print("Starting frontend development server")
-    yarn(c, "yarn run dev")
+    print('Starting frontend development server')
+    yarn(c, 'yarn run dev')
 
 
 @task(
     help={
-        'ref': "git ref, default: current git ref",
-        'tag': "git tag to look for release",
-        'file': "destination to frontend-build.zip file",
-        'repo': "GitHub repository, default: InvenTree/inventree",
-        'extract': "Also extract and place at the correct destination, default: True",
-        'clean': "Delete old files from InvenTree/web/static/web first, default: True",
+        'ref': 'git ref, default: current git ref',
+        'tag': 'git tag to look for release',
+        'file': 'destination to frontend-build.zip file',
+        'repo': 'GitHub repository, default: InvenTree/inventree',
+        'extract': 'Also extract and place at the correct destination, default: True',
+        'clean': 'Delete old files from InvenTree/web/static/web first, default: True',
     }
 )
 def frontend_download(
@@ -903,7 +903,7 @@ def frontend_download(
     ref=None,
     tag=None,
     file=None,
-    repo="InvenTree/inventree",
+    repo='InvenTree/inventree',
     extract=True,
     clean=True,
 ):
@@ -928,7 +928,7 @@ def frontend_download(
     import requests
 
     # globals
-    default_headers = {"Accept": "application/vnd.github.v3+json"}
+    default_headers = {'Accept': 'application/vnd.github.v3+json'}
 
     # helper functions
     def find_resource(resource, key, value):
@@ -942,34 +942,34 @@ def frontend_download(
         if not extract:
             return
 
-        dest_path = Path(__file__).parent / "InvenTree/web/static/web"
+        dest_path = Path(__file__).parent / 'InvenTree/web/static/web'
 
         # if clean, delete static/web directory
         if clean:
             shutil.rmtree(dest_path, ignore_errors=True)
             os.makedirs(dest_path)
-            print(f"Cleaned directory: {dest_path}")
+            print(f'Cleaned directory: {dest_path}')
 
         # unzip build to static folder
-        with ZipFile(file, "r") as zip_ref:
+        with ZipFile(file, 'r') as zip_ref:
             zip_ref.extractall(dest_path)
 
-        print(f"Unzipped downloaded frontend build to: {dest_path}")
+        print(f'Unzipped downloaded frontend build to: {dest_path}')
 
     def handle_download(url):
         # download frontend-build.zip to temporary file
         with requests.get(
             url, headers=default_headers, stream=True, allow_redirects=True
-        ) as response, NamedTemporaryFile(suffix=".zip") as dst:
+        ) as response, NamedTemporaryFile(suffix='.zip') as dst:
             response.raise_for_status()
 
             # auto decode the gzipped raw data
             response.raw.read = functools.partial(
                 response.raw.read, decode_content=True
             )
-            with open(dst.name, "wb") as f:
+            with open(dst.name, 'wb') as f:
                 shutil.copyfileobj(response.raw, f)
-            print(f"Downloaded frontend build to temporary file: {dst.name}")
+            print(f'Downloaded frontend build to temporary file: {dst.name}')
 
             handle_extract(dst.name)
 
@@ -980,26 +980,26 @@ def frontend_download(
 
     # check arguments
     if ref is not None and tag is not None:
-        print("[ERROR] Do not set ref and tag.")
+        print('[ERROR] Do not set ref and tag.')
         return
 
     if ref is None and tag is None:
         try:
             ref = subprocess.check_output(
-                ["git", "rev-parse", "HEAD"], encoding="utf-8"
+                ['git', 'rev-parse', 'HEAD'], encoding='utf-8'
             ).strip()
         except Exception:
             print("[ERROR] Cannot get current ref via 'git rev-parse HEAD'")
             return
 
     if ref is None and tag is None:
-        print("[ERROR] Either ref or tag needs to be set.")
+        print('[ERROR] Either ref or tag needs to be set.')
 
     if tag:
-        tag = tag.lstrip("v")
+        tag = tag.lstrip('v')
         try:
             handle_download(
-                f"https://github.com/{repo}/releases/download/{tag}/frontend-build.zip"
+                f'https://github.com/{repo}/releases/download/{tag}/frontend-build.zip'
             )
         except Exception as e:
             if not isinstance(e, requests.HTTPError):
@@ -1015,12 +1015,12 @@ Then try continuing by running: invoke frontend-download --file <path-to-downloa
     if ref:
         # get workflow run from all workflow runs on that particular ref
         workflow_runs = requests.get(
-            f"https://api.github.com/repos/{repo}/actions/runs?head_sha={ref}",
+            f'https://api.github.com/repos/{repo}/actions/runs?head_sha={ref}',
             headers=default_headers,
         ).json()
 
-        if not (qc_run := find_resource(workflow_runs["workflow_runs"], "name", "QC")):
-            print("[ERROR] Cannot find any workflow runs for current sha")
+        if not (qc_run := find_resource(workflow_runs['workflow_runs'], 'name', 'QC')):
+            print('[ERROR] Cannot find any workflow runs for current sha')
             return
         print(
             f"Found workflow {qc_run['name']} (run {qc_run['run_number']}-{qc_run['run_attempt']})"
@@ -1028,14 +1028,14 @@ Then try continuing by running: invoke frontend-download --file <path-to-downloa
 
         # get frontend-build artifact from all artifacts available for this workflow run
         artifacts = requests.get(
-            qc_run["artifacts_url"], headers=default_headers
+            qc_run['artifacts_url'], headers=default_headers
         ).json()
         if not (
             frontend_artifact := find_resource(
-                artifacts["artifacts"], "name", "frontend-build"
+                artifacts['artifacts'], 'name', 'frontend-build'
             )
         ):
-            print("[ERROR] Cannot find frontend-build.zip attachment for current sha")
+            print('[ERROR] Cannot find frontend-build.zip attachment for current sha')
             return
         print(
             f"Found artifact {frontend_artifact['name']} with id {frontend_artifact['id']} ({frontend_artifact['size_in_bytes']/1e6:.2f}MB)."