diff --git a/InvenTree/InvenTree/config.py b/InvenTree/InvenTree/config.py
index 1ddbc80d6c..4e54f62a19 100644
--- a/InvenTree/InvenTree/config.py
+++ b/InvenTree/InvenTree/config.py
@@ -281,6 +281,12 @@ def get_plugin_file():
     return plugin_file
 
 
+def get_plugin_dir():
+    """Returns the path of the custom plugins directory"""
+
+    return get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
+
+
 def get_secret_key():
     """Return the secret key value which will be used by django.
 
diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py
index a7cba2fd97..a7652f8969 100644
--- a/InvenTree/InvenTree/urls.py
+++ b/InvenTree/InvenTree/urls.py
@@ -189,10 +189,6 @@ classic_frontendpatterns = [
     re_path(r'^about/', AboutView.as_view(), name='about'),
     re_path(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
 
-    # admin sites
-    re_path(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')),
-    re_path(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'),
-
     # DB user sessions
     path('accounts/sessions/other/delete/', view=CustomSessionDeleteOtherView.as_view(), name='session_delete_other', ),
     re_path(r'^accounts/sessions/(?P<pk>\w+)/delete/$', view=CustomSessionDeleteView.as_view(), name='session_delete', ),
@@ -213,22 +209,26 @@ classic_frontendpatterns = [
 
 new_frontendpatterns = platform_urls
 
-# Load patterns for frontend according to settings
-frontendpatterns = []
-if settings.ENABLE_CLASSIC_FRONTEND:
-    frontendpatterns.append(re_path('', include(classic_frontendpatterns)))
-if settings.ENABLE_PLATFORM_FRONTEND:
-    frontendpatterns.append(re_path('', include(new_frontendpatterns)))
+urlpatterns = [
+    # admin sites
+    re_path(f'^{settings.INVENTREE_ADMIN_URL}/error_log/', include('error_report.urls')),
+    re_path(f'^{settings.INVENTREE_ADMIN_URL}/', admin.site.urls, name='inventree-admin'),
+]
 
+urlpatterns += backendpatterns
+
+frontendpatterns = []
+
+if settings.ENABLE_CLASSIC_FRONTEND:
+    frontendpatterns += classic_frontendpatterns
+if settings.ENABLE_PLATFORM_FRONTEND:
+    frontendpatterns += new_frontendpatterns
+
+urlpatterns += frontendpatterns
 
 # Append custom plugin URLs (if plugin support is enabled)
 if settings.PLUGINS_ENABLED:
-    frontendpatterns.append(get_plugin_urls())
-
-urlpatterns = [
-    re_path('', include(frontendpatterns)),
-    re_path('', include(backendpatterns)),
-]
+    urlpatterns.append(get_plugin_urls())
 
 # Server running in "DEBUG" mode?
 if settings.DEBUG:
@@ -245,5 +245,10 @@ if settings.DEBUG:
             path('__debug__/', include(debug_toolbar.urls)),
         ] + urlpatterns
 
+# Redirect for favicon.ico
+urlpatterns.append(
+    path('favicon.ico', RedirectView.as_view(url=f'{settings.STATIC_ROOT}/img/favicon/favicon.ico'))
+)
+
 # Send any unknown URLs to the parts page
 urlpatterns += [re_path(r'^.*$', RedirectView.as_view(url='/index/', permanent=False), name='index')]
diff --git a/InvenTree/plugin/base/integration/AppMixin.py b/InvenTree/plugin/base/integration/AppMixin.py
index 4334dabaaf..8c3ae557eb 100644
--- a/InvenTree/plugin/base/integration/AppMixin.py
+++ b/InvenTree/plugin/base/integration/AppMixin.py
@@ -1,11 +1,14 @@
 """Plugin mixin class for AppMixin."""
 import logging
 from importlib import reload
+from pathlib import Path
 
 from django.apps import apps
 from django.conf import settings
 from django.contrib import admin
 
+from InvenTree.config import get_plugin_dir
+
 logger = logging.getLogger('inventree')
 
 
@@ -156,12 +159,22 @@ class AppMixin:
         - a local file / dir
         - a package
         """
-        try:
-            # for local path plugins
-            plugin_path = '.'.join(plugin.path().relative_to(settings.BASE_DIR).parts)
-        except ValueError:  # pragma: no cover
+        path = plugin.path()
+        custom_plugins_dir = get_plugin_dir()
+
+        if path.is_relative_to(settings.BASE_DIR):
+            # Plugins which are located relative to the base code directory
+            plugin_path = '.'.join(path.relative_to(settings.BASE_DIR).parts)
+        elif custom_plugins_dir and path.is_relative_to(custom_plugins_dir):
+            # Plugins which are located relative to the custom plugins directory
+            plugin_path = '.'.join(path.relative_to(custom_plugins_dir).parts)
+
+            # Ensure that the parent directory is added also
+            plugin_path = Path(custom_plugins_dir).parts[-1] + '.' + plugin_path
+        else:
             # plugin is shipped as package - extract plugin module name
             plugin_path = plugin.__module__.split('.')[0]
+
         return plugin_path
 
 # endregion
diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py
index 3fd402d471..999765edb8 100644
--- a/InvenTree/plugin/registry.py
+++ b/InvenTree/plugin/registry.py
@@ -17,14 +17,14 @@ from django.apps import apps
 from django.conf import settings
 from django.contrib import admin
 from django.db.utils import IntegrityError, OperationalError, ProgrammingError
-from django.urls import clear_url_caches, include, re_path
+from django.urls import clear_url_caches, re_path
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 
 from maintenance_mode.core import (get_maintenance_mode, maintenance_mode_on,
                                    set_maintenance_mode)
 
-from InvenTree.config import get_setting
+from InvenTree.config import get_plugin_dir
 from InvenTree.ready import canAppAccessDatabase
 
 from .helpers import (IntegrationPluginError, get_entrypoints, get_plugins,
@@ -239,7 +239,7 @@ class PluginsRegistry:
             if settings.TESTING:
                 custom_dirs = os.getenv('INVENTREE_PLUGIN_TEST_DIR', None)
             else:  # pragma: no cover
-                custom_dirs = get_setting('INVENTREE_PLUGIN_DIR', 'plugin_dir')
+                custom_dirs = get_plugin_dir()
 
                 # Load from user specified directories (unless in testing mode)
                 dirs.append('plugins')
@@ -577,19 +577,28 @@ class PluginsRegistry:
         self.plugins_full: Dict[str, InvenTreePlugin] = {}
 
     def _update_urls(self):
-        from InvenTree.urls import frontendpatterns as urlpattern
-        from InvenTree.urls import urlpatterns as global_pattern
+        """Due to the order in which plugins are loaded, the patterns in urls.py may be out of date.
+
+        This function updates the patterns in urls.py to ensure that the correct patterns are loaded,
+        and then refreshes the django url cache.
+
+        Note that we also have to refresh the admin site URLS,
+        as any custom AppMixin plugins require admin integration
+        """
+
+        from InvenTree.urls import urlpatterns
         from plugin.urls import get_plugin_urls
 
-        for index, url in enumerate(urlpattern):
-            if hasattr(url, 'app_name'):
-                if url.app_name == 'admin':
-                    urlpattern[index] = re_path(r'^admin/', admin.site.urls, name='inventree-admin')
-                elif url.app_name == 'plugin':
-                    urlpattern[index] = get_plugin_urls()
+        for index, url in enumerate(urlpatterns):
 
-        # Replace frontendpatterns
-        global_pattern[0] = re_path('', include(urlpattern))
+            app_name = getattr(url, 'app_name', None)
+
+            if app_name == 'admin':
+                urlpatterns[index] = re_path(r'^admin/', admin.site.urls, name='inventree-admin')
+            if app_name == 'plugin':
+                urlpatterns[index] = get_plugin_urls()
+
+        # Refresh the URL cache
         clear_url_caches()
     # endregion