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`,