diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py
index ab243f423d..7390d5136e 100644
--- a/src/backend/InvenTree/InvenTree/api_version.py
+++ b/src/backend/InvenTree/InvenTree/api_version.py
@@ -1,13 +1,17 @@
"""InvenTree API version information."""
# InvenTree API version
-INVENTREE_API_VERSION = 326
+INVENTREE_API_VERSION = 327
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
INVENTREE_API_TEXT = """
+v327 - 2025-03-20 : https://github.com/inventree/InvenTree/pull/9339
+ - Adds "is_mandatory" field to the Plugin API
+ - Adds ability to filter by "mandatory" status in the Plugin API
+
v326 - 2025-03-18 : https://github.com/inventree/InvenTree/pull/9096
- Overhaul the data-export API functionality
- Allow customization of data exporting via plugins
diff --git a/src/backend/InvenTree/plugin/api.py b/src/backend/InvenTree/plugin/api.py
index c20fd9d0ee..f0f8a93808 100644
--- a/src/backend/InvenTree/plugin/api.py
+++ b/src/backend/InvenTree/plugin/api.py
@@ -18,6 +18,7 @@ import plugin.serializers as PluginSerializers
from common.api import GlobalSettingsPermissions
from InvenTree.api import MetadataView
from InvenTree.filters import SEARCH_ORDER_FILTER
+from InvenTree.helpers import str2bool
from InvenTree.mixins import (
CreateAPI,
ListAPI,
@@ -27,13 +28,13 @@ from InvenTree.mixins import (
UpdateAPI,
)
from InvenTree.permissions import IsSuperuser, IsSuperuserOrReadOnly
-from plugin import registry
from plugin.base.action.api import ActionPluginView
from plugin.base.barcodes.api import barcode_api_urls
from plugin.base.locate.api import LocatePluginView
from plugin.base.ui.api import ui_plugins_api_urls
from plugin.models import PluginConfig, PluginSetting
from plugin.plugin import InvenTreePlugin
+from plugin.registry import registry
class PluginFilter(rest_filters.FilterSet):
@@ -75,7 +76,7 @@ class PluginFilter(rest_filters.FilterSet):
return queryset.filter(pk__in=matches)
builtin = rest_filters.BooleanFilter(
- field_name='builtin', label='Builtin', method='filter_builtin'
+ field_name='builtin', label=_('Builtin'), method='filter_builtin'
)
def filter_builtin(self, queryset, name, value):
@@ -88,8 +89,19 @@ class PluginFilter(rest_filters.FilterSet):
return queryset.filter(pk__in=matches)
+ mandatory = rest_filters.BooleanFilter(
+ field_name='mandatory', label=_('Mandatory'), method='filter_mandatory'
+ )
+
+ def filter_mandatory(self, queryset, name, value):
+ """Filter by 'mandatory' flag."""
+ if str2bool(value):
+ return queryset.filter(key__in=registry.MANDATORY_PLUGINS)
+ else:
+ return queryset.exclude(key__in=registry.MANDATORY_PLUGINS)
+
sample = rest_filters.BooleanFilter(
- field_name='sample', label='Sample', method='filter_sample'
+ field_name='sample', label=_('Sample'), method='filter_sample'
)
def filter_sample(self, queryset, name, value):
@@ -103,7 +115,7 @@ class PluginFilter(rest_filters.FilterSet):
return queryset.filter(pk__in=matches)
installed = rest_filters.BooleanFilter(
- field_name='installed', label='Installed', method='filter_installed'
+ field_name='installed', label=_('Installed'), method='filter_installed'
)
def filter_installed(self, queryset, name, value):
@@ -135,8 +147,6 @@ class PluginList(ListAPI):
filter_backends = SEARCH_ORDER_FILTER
- filterset_fields = ['active']
-
ordering_fields = ['key', 'name', 'active']
ordering = ['-active', 'name', 'key']
diff --git a/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py b/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py
index cab309065a..31635af4ce 100644
--- a/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py
+++ b/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py
@@ -15,6 +15,11 @@ class SupplierBarcodeTests(InvenTreeAPITestCase):
SCAN_URL = reverse('api-barcode-scan')
+ def setUp(self):
+ """Ensure the digikey plugin is enabled."""
+ super().setUp()
+ registry.set_plugin_state('digikeyplugin', True)
+
@classmethod
def setUpTestData(cls):
"""Create supplier parts for barcodes."""
diff --git a/src/backend/InvenTree/plugin/models.py b/src/backend/InvenTree/plugin/models.py
index 3068dbcfad..c5947c24f5 100644
--- a/src/backend/InvenTree/plugin/models.py
+++ b/src/backend/InvenTree/plugin/models.py
@@ -13,8 +13,9 @@ from django.utils.translation import gettext_lazy as _
import common.models
import InvenTree.models
import plugin.staticfiles
-from plugin import InvenTreePlugin, registry
+from plugin import InvenTreePlugin
from plugin.events import PluginEvents, trigger_event
+from plugin.registry import registry
class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
@@ -146,8 +147,8 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
super().save(force_insert, force_update, *args, **kwargs)
- if self.is_builtin():
- # Force active if builtin
+ if self.is_mandatory():
+ # Force active if mandatory plugin
self.active = True
if not no_reload and self.active != self.__org_active:
@@ -180,6 +181,11 @@ class PluginConfig(InvenTree.models.MetadataMixin, models.Model):
return self.plugin.check_is_builtin()
+ @admin.display(boolean=True, description=_('Mandatory Plugin'))
+ def is_mandatory(self) -> bool:
+ """Return True if this plugin is mandatory."""
+ return self.key in registry.MANDATORY_PLUGINS
+
@admin.display(boolean=True, description=_('Package Plugin'))
def is_package(self) -> bool:
"""Return True if this is a 'package' plugin."""
diff --git a/src/backend/InvenTree/plugin/registry.py b/src/backend/InvenTree/plugin/registry.py
index 81b5b3ed16..f5debcda61 100644
--- a/src/backend/InvenTree/plugin/registry.py
+++ b/src/backend/InvenTree/plugin/registry.py
@@ -53,6 +53,21 @@ class PluginsRegistry:
DEFAULT_MIXIN_ORDER = [SettingsMixin, ScheduleMixin, AppMixin, UrlsMixin]
+ # This list of plugins are *always* enabled, and are loaded by default
+ # This is because they provide core functionality to the InvenTree system
+ # Other 'builtin' plugins are automatically loaded, but can be disabled by the user
+ MANDATORY_PLUGINS = [
+ 'inventreebarcode',
+ 'bom-exporter',
+ 'inventree-exporter',
+ 'inventreecorenotificationsplugin',
+ 'inventreecurrencyexchange',
+ 'inventreecorenotificationsplugin',
+ 'inventreelabel',
+ 'inventreelabelmachine',
+ 'inventreelabelsheet',
+ ]
+
def __init__(self) -> None:
"""Initialize registry.
@@ -518,10 +533,11 @@ class PluginsRegistry:
if getattr(plugin, 'is_package', False):
package_name = getattr(plugin, 'package_name', None)
- # Auto-enable builtin plugins
- if builtin and plg_db and not plg_db.active:
- plg_db.active = True
- plg_db.save()
+ # Auto-enable default builtin plugins
+ if builtin and plg_db and plg_db.is_mandatory():
+ if not plg_db.active:
+ plg_db.active = True
+ plg_db.save()
# Save the package_name attribute to the plugin
if plg_db.package_name != package_name:
diff --git a/src/backend/InvenTree/plugin/serializers.py b/src/backend/InvenTree/plugin/serializers.py
index c6e9981a77..6ec679c78e 100644
--- a/src/backend/InvenTree/plugin/serializers.py
+++ b/src/backend/InvenTree/plugin/serializers.py
@@ -59,12 +59,14 @@ class PluginConfigSerializer(serializers.ModelSerializer):
'is_sample',
'is_installed',
'is_package',
+ 'is_mandatory',
]
read_only_fields = ['key', 'is_builtin', 'is_sample', 'is_installed']
meta = serializers.DictField(read_only=True)
mixins = serializers.DictField(read_only=True)
+ is_mandatory = serializers.BooleanField(read_only=True)
class PluginAdminDetailSerializer(serializers.ModelSerializer):
diff --git a/src/frontend/src/tables/plugin/PluginListTable.tsx b/src/frontend/src/tables/plugin/PluginListTable.tsx
index f8218b899b..e28bfd4c7a 100644
--- a/src/frontend/src/tables/plugin/PluginListTable.tsx
+++ b/src/frontend/src/tables/plugin/PluginListTable.tsx
@@ -14,7 +14,6 @@ import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ActionButton } from '../../components/buttons/ActionButton';
-import { YesNoButton } from '../../components/buttons/YesNoButton';
import { DetailDrawer } from '../../components/nav/DetailDrawer';
import PluginDrawer from '../../components/plugins/PluginDrawer';
import type { PluginInterface } from '../../components/plugins/PluginInterface';
@@ -29,6 +28,7 @@ import { useTable } from '../../hooks/UseTable';
import { apiUrl, useServerApiState } from '../../states/ApiState';
import { useUserState } from '../../states/UserState';
import type { TableColumn } from '../Column';
+import { BooleanColumn } from '../ColumnRenderers';
import { InvenTreeTable } from '../InvenTreeTable';
import type { RowAction } from '../RowActions';
@@ -89,12 +89,21 @@ export default function PluginListTable() {
);
}
},
- {
+ BooleanColumn({
accessor: 'active',
sortable: true,
- title: t`Active`,
- render: (record: any) =>
- },
+ title: t`Active`
+ }),
+ BooleanColumn({
+ accessor: 'is_builtin',
+ sortable: false,
+ title: t`Builtin`
+ }),
+ BooleanColumn({
+ accessor: 'is_mandatory',
+ sortable: false,
+ title: t`Mandatory`
+ }),
{
accessor: 'meta.description',
title: t`Description`,
@@ -164,7 +173,7 @@ export default function PluginListTable() {
return [
{
- hidden: record.is_builtin != false || record.active != true,
+ hidden: record.is_mandatory != false || record.active != true,
title: t`Deactivate`,
color: 'red',
icon: ,
@@ -176,7 +185,7 @@ export default function PluginListTable() {
},
{
hidden:
- record.is_builtin != false ||
+ record.is_mandatory != false ||
!record.is_installed ||
record.active != false,
title: t`Activate`,
@@ -207,6 +216,7 @@ export default function PluginListTable() {
!user.isSuperuser() ||
record.active ||
record.is_builtin ||
+ record.is_mandatory ||
record.is_sample ||
!record.is_installed ||
!record.is_package,
@@ -225,6 +235,7 @@ export default function PluginListTable() {
hidden:
record.active ||
record.is_builtin ||
+ record.is_mandatory ||
record.is_sample ||
record.is_installed ||
!user.isSuperuser(),
@@ -405,6 +416,11 @@ export default function PluginListTable() {
label: t`Builtin`,
type: 'boolean'
},
+ {
+ name: 'mandatory',
+ label: t`Mandatory`,
+ type: 'boolean'
+ },
{
name: 'sample',
label: t`Sample`,