diff --git a/docs/docs/concepts/notifications.md b/docs/docs/concepts/notifications.md new file mode 100644 index 000000000000..582ad871a880 --- /dev/null +++ b/docs/docs/concepts/notifications.md @@ -0,0 +1,73 @@ +--- +title: Notifications +--- + +## Notifications System + +InvenTree provides a notification system which allows users to receive notifications about various events that occur within the system. + +## Notification Types + +### User Interface + +Notifications are displayed in the user interface. New notifications are announced in the header. +{% with id="notification_header", url="part/notification_header.png", description="One new notification in the header" %} +{% include 'img.html' %} +{% endwith %} + +They can be viewed in a flyout. +{% with id="notification_flyout", url="part/notification_flyout.png", description="One new notification in the flyout" %} +{% include 'img.html' %} +{% endwith %} + +All current notifications are listed in the inbox. +{% with id="notification_inbox", url="part/notification_inbox.png", description="One new notification in the notification inbox" %} +{% include 'img.html' %} +{% endwith %} + +All past notification are listed in the history. They can be deleted one-by-one or all at once from there. +{% with id="notification_history", url="part/notification_history.png", description="One old notification in the notification history" %} +{% include 'img.html' %} +{% endwith %} + +### Email + +Users can also receive notifications via email. This is particularly useful for important events that require immediate attention. + +!!! warning "Email Configuration Required" + External notifications require correct [email configuration](../start/config.md#email-settings). They also need to be enabled in the settings under notifications`. + +!!! warning "Valid Email Address" + Each user must have a valid email address associated with their account to receive email notifications + +### Third-Party Integrations + +In addition to the built-in notification system, InvenTree can be integrated with third-party services such as Slack or Discord to send notifications. This allows users to receive real-time updates in their preferred communication channels. + +Third party integrations rely on the [plugin system](../plugins/index.md) to provide the necessary functionality. + +## Notification Options + +The following [global settings](../settings/global.md) control the notification system: + +| Name | Description | Default | Units | +| ---- | ----------- | ------- | ----- | +{{ globalsetting("NOTIFICATIONS_ENABLE") }} +{{ globalsetting("NOTIFICATIONS_ERRORS") }} +{{ globalsetting("NOTIFICATIONS_LOW_STOCK") }} +{{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }} + +## Notification Categories + +### Error Notifications + +If enabled, InvenTree can send notifications about errors that occur within the system. This is useful for administrators to monitor the health of the application and address issues promptly. + +!!! info "Staff Only" + Error notifications are only sent to staff users. Regular users will not receive these notifications. + +### Part Notifications + +There are many types of notifications associated with parts in InvenTree. These notifications can be configured to alert users about various events, such as low stock levels, build orders, and more. + +Read the [part notification documentation](../part/notification.md) for more details. diff --git a/docs/docs/part/notification.md b/docs/docs/part/notification.md index 745495434fee..b03e51280185 100644 --- a/docs/docs/part/notification.md +++ b/docs/docs/part/notification.md @@ -2,38 +2,10 @@ title: Part Notifications --- -## General Notification Details - -Users can select to receive notifications when certain events occur. - -!!! warning "Email Configuration Required" - External notifications require correct [email configuration](../start/config.md#email-settings). They also need to be enabled in the settings under notifications`. - -!!! warning "Valid Email Address" - Each user must have a valid email address associated with their account to receive email notifications - -Notifications are also shown in the user interface. New notifications are announced in the header. -{% with id="notification_header", url="part/notification_header.png", description="One new notification in the header" %} -{% include 'img.html' %} -{% endwith %} - -They can be viewed in a flyout. -{% with id="notification_flyout", url="part/notification_flyout.png", description="One new notification in the flyout" %} -{% include 'img.html' %} -{% endwith %} - -All current notifications are listed in the inbox. -{% with id="notification_inbox", url="part/notification_inbox.png", description="One new notification in the notification inbox" %} -{% include 'img.html' %} -{% endwith %} - -All past notification are listed in the history. They can be deleted one-by-one or all at once from there. -{% with id="notification_history", url="part/notification_history.png", description="One old notification in the notification history" %} -{% include 'img.html' %} -{% endwith %} - ## Part Notification Events +Multiple events can trigger [notifications](../concepts/notifications.md) related to *Parts* in InvenTree. These notifications can be delivered via the user interface, email, or third-party integrations. + ### Low Stock Notification If the *minimum stock* threshold is set for a *Part*, then a "low stock" notification can be generated when the stock level for that part falls below the configured level. diff --git a/docs/docs/settings/global.md b/docs/docs/settings/global.md index 68bfbcd0d98c..eaec5db7bea8 100644 --- a/docs/docs/settings/global.md +++ b/docs/docs/settings/global.md @@ -36,7 +36,6 @@ Configuration of basic server settings: {{ globalsetting("INVENTREE_BACKUP_DAYS") }} {{ globalsetting("INVENTREE_DELETE_TASKS_DAYS") }} {{ globalsetting("INVENTREE_DELETE_ERRORS_DAYS") }} -{{ globalsetting("INVENTREE_DELETE_NOTIFICATIONS_DAYS") }} ### Login Settings diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 08e9b73e1ed0..1b351e194c6b 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -82,6 +82,7 @@ nav: - Physical Units: concepts/units.md - Companies: concepts/company.md - Custom States: concepts/custom_states.md + - Notifications: concepts/notifications.md - Pricing: concepts/pricing.md - Project Codes: concepts/project_codes.md - Barcodes: diff --git a/src/backend/InvenTree/InvenTree/models.py b/src/backend/InvenTree/InvenTree/models.py index a70f5b601ae5..85ebfc753a07 100644 --- a/src/backend/InvenTree/InvenTree/models.py +++ b/src/backend/InvenTree/InvenTree/models.py @@ -1035,6 +1035,13 @@ def notify_staff_users_of_error(instance, label: str, context: dict): """Helper function to notify staff users of an error.""" import common.models import common.notifications + import common.settings + + # Return early if error notifications are not enabled + if not common.settings.get_global_setting( + 'NOTIFICATIONS_ERRORS', backup_value=True + ): + return try: # Get all staff users diff --git a/src/backend/InvenTree/common/notifications.py b/src/backend/InvenTree/common/notifications.py index 26f1935de8b5..a816eef5c05d 100644 --- a/src/backend/InvenTree/common/notifications.py +++ b/src/backend/InvenTree/common/notifications.py @@ -13,6 +13,7 @@ import common.models import InvenTree.helpers +from common.settings import notifications_enabled from InvenTree.ready import isImportingData, isRebuildingData from plugin import registry from plugin.models import NotificationUserSetting, PluginConfig @@ -374,6 +375,10 @@ def trigger_notification( if isImportingData() or isRebuildingData(): return + # Ignore if notifications are disabled globally + if not notifications_enabled(): + return + targets = kwargs.get('targets') target_fnc = kwargs.get('target_fnc') target_args = kwargs.get('target_args', []) diff --git a/src/backend/InvenTree/common/setting/system.py b/src/backend/InvenTree/common/setting/system.py index ff54f947143f..cfad8dbbfeaa 100644 --- a/src/backend/InvenTree/common/setting/system.py +++ b/src/backend/InvenTree/common/setting/system.py @@ -15,7 +15,6 @@ import build.validators import common.currency -import common.models import common.validators import order.validators import report.helpers @@ -309,6 +308,24 @@ def __call__(self, value): 'units': _('days'), 'validator': [int, MinValueValidator(7)], }, + 'NOTIFICATIONS_ENABLE': { + 'name': _('Enable Notifications'), + 'description': _('Enable user notifications for system events'), + 'default': True, + 'validator': bool, + }, + 'NOTIFICATIONS_ERRORS': { + 'name': _('Error Notifications'), + 'description': _('Enable notifications for system errors'), + 'default': True, + 'validator': bool, + }, + 'NOTIFICATIONS_LOW_STOCK': { + 'name': _('Low Stock Notifications'), + 'description': _('Enable notifications when a part is low on stock'), + 'default': True, + 'validator': bool, + }, 'INVENTREE_DELETE_NOTIFICATIONS_DAYS': { 'name': _('Notification Deletion Interval'), 'description': _( diff --git a/src/backend/InvenTree/common/settings.py b/src/backend/InvenTree/common/settings.py index e1bc11f51824..5602a3d12136 100644 --- a/src/backend/InvenTree/common/settings.py +++ b/src/backend/InvenTree/common/settings.py @@ -28,14 +28,25 @@ def set_global_setting(key, value, change_user=None, create=True, **kwargs): return InvenTreeSetting.set_setting(key, value, **kwargs) -def stock_expiry_enabled(): - """Returns True if the stock expiry feature is enabled.""" +def stock_expiry_enabled() -> bool: + """Helper function which returns True if the stock expiry feature is enabled.""" from common.models import InvenTreeSetting - return InvenTreeSetting.get_setting('STOCK_ENABLE_EXPIRY', False, create=False) + return InvenTreeSetting.get_setting( + 'STOCK_ENABLE_EXPIRY', backup_value=False, create=False + ) + + +def notifications_enabled() -> bool: + """Helper function which returns True if the notifications feature is enabled.""" + from common.models import InvenTreeSetting + + return InvenTreeSetting.get_setting( + 'NOTIFICATIONS_ENABLE', backup_value=True, create=False + ) -def prevent_build_output_complete_on_incompleted_tests(): +def prevent_build_output_complete_on_incompleted_tests() -> bool: """Returns True if the completion of the build outputs is disabled until the required tests are passed.""" from common.models import InvenTreeSetting diff --git a/src/backend/InvenTree/part/tasks.py b/src/backend/InvenTree/part/tasks.py index 8d70537fb39a..35fcf9bc2b16 100644 --- a/src/backend/InvenTree/part/tasks.py +++ b/src/backend/InvenTree/part/tasks.py @@ -12,12 +12,11 @@ import common.currency import common.notifications import company.models -import InvenTree.helpers import InvenTree.helpers_model import InvenTree.tasks import part.models as part_models import part.stocktake -from common.settings import get_global_setting +from common.settings import get_global_setting, notifications_enabled from InvenTree.tasks import ( ScheduledTask, check_daily_holdoff, @@ -35,6 +34,9 @@ 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 """ + if not get_global_setting('NOTIFICATIONS_LOW_STOCK', backup_value=True): + return + name = _('Low stock notification') message = _( f'The available stock for {part.name} has fallen below the configured minimum level' @@ -52,11 +54,19 @@ def notify_low_stock(part: part_models.Part): ) -def notify_low_stock_if_required(part_id: int): +def notify_low_stock_if_required(part_id: int) -> None: """Check if the stock quantity has fallen below the minimum threshold of part. If true, notify the users who have subscribed to the part """ + # Return early if notifications are not enabled + # This prevents additional tasks from being queued unnecessarily + if not notifications_enabled(): + return + + if not get_global_setting('NOTIFICATIONS_LOW_STOCK', backup_value=True): + return + try: part = part_models.Part.objects.get(pk=part_id) except part_models.Part.DoesNotExist: diff --git a/src/backend/InvenTree/part/test_part.py b/src/backend/InvenTree/part/test_part.py index 7931926945a5..e00ca649f4aa 100644 --- a/src/backend/InvenTree/part/test_part.py +++ b/src/backend/InvenTree/part/test_part.py @@ -811,15 +811,16 @@ def _notification_run(self, run_class=None): self.part.set_starred(self.user, True) self.part.save() - # There should be 1 (or 2) notifications - in some cases an error is generated, which creates a subsequent notification - self.assertIn(NotificationEntry.objects.all().count(), [1, 2]) - class PartNotificationTest(BaseNotificationIntegrationTest): """Integration test for part notifications.""" def test_notification(self): """Test that a notification is generated.""" + # Ensure notifications are enabled + set_global_setting('NOTIFICATIONS_ENABLE', True) + set_global_setting('NOTIFICATIONS_LOW_STOCK', True) + self._notification_run(UIMessageNotification) # There should be 1 notification message right now @@ -830,3 +831,19 @@ def test_notification(self): # There should not be more messages self.assertEqual(NotificationMessage.objects.all().count(), 1) + + def test_disabled(self): + """Test that the notification is not generated if notifications are globally disabled.""" + set_global_setting('NOTIFICATIONS_ENABLE', False) + set_global_setting('NOTIFICATIONS_LOW_STOCK', True) + + self._notification_run(UIMessageNotification) + self.assertEqual(NotificationEntry.objects.all().count(), 0) + + def test_disabled_low_stock(self): + """Test that the notification is not generated if low stock notifications are disabled.""" + set_global_setting('NOTIFICATIONS_ENABLE', True) + set_global_setting('NOTIFICATIONS_LOW_STOCK', False) + + self._notification_run(UIMessageNotification) + self.assertEqual(NotificationEntry.objects.all().count(), 0) diff --git a/src/frontend/src/components/nav/Header.tsx b/src/frontend/src/components/nav/Header.tsx index d6e2b12eb815..9eb2ec421c4d 100644 --- a/src/frontend/src/components/nav/Header.tsx +++ b/src/frontend/src/components/nav/Header.tsx @@ -145,24 +145,26 @@ export function Header() { {globalSettings.isSet('BARCODE_ENABLE') && } - - - - - - - + {globalSettings.isSet('NOTIFICATIONS_ENABLE') && ( + + + + + + + + )} diff --git a/src/frontend/src/components/nav/NavigationDrawer.tsx b/src/frontend/src/components/nav/NavigationDrawer.tsx index 35ffe6952143..c488dc2dbbf2 100644 --- a/src/frontend/src/components/nav/NavigationDrawer.tsx +++ b/src/frontend/src/components/nav/NavigationDrawer.tsx @@ -140,7 +140,8 @@ function DrawerContent({ closeFunc }: Readonly<{ closeFunc?: () => void }>) { id: 'notifications', title: t`Notifications`, link: '/notifications', - icon: 'notification' + icon: 'notification', + hidden: !globalSettings.isSet('NOTIFICATIONS_ENABLE') }, { id: 'user-settings', @@ -163,7 +164,7 @@ function DrawerContent({ closeFunc }: Readonly<{ closeFunc?: () => void }>) { hidden: !user.isStaff() } ]; - }, [user]); + }, [globalSettings, user]); const menuItemsDocumentation: MenuLinkItem[] = useMemo( () => DocumentationLinks(), diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index e77831655ab1..d626aa0781ed 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -1,12 +1,11 @@ import { t } from '@lingui/core/macro'; -import { Alert, Skeleton, Stack, Text } from '@mantine/core'; +import { Skeleton, Stack } from '@mantine/core'; import { IconBellCog, IconCategory, IconCurrencyDollar, IconFileAnalytics, IconFingerprint, - IconInfoCircle, IconPackages, IconQrcode, IconServerCog, @@ -57,9 +56,7 @@ export default function SystemSettings() { 'INVENTREE_STRICT_URLS', 'INVENTREE_BACKUP_ENABLE', 'INVENTREE_BACKUP_DAYS', - 'INVENTREE_DELETE_TASKS_DAYS', - 'INVENTREE_DELETE_ERRORS_DAYS', - 'INVENTREE_DELETE_NOTIFICATIONS_DAYS' + 'INVENTREE_DELETE_TASKS_DAYS' ]} /> ) @@ -113,15 +110,14 @@ export default function SystemSettings() { label: t`Notifications`, icon: , content: ( - - } - > - This panel has not yet been implemented - - + ) }, { diff --git a/src/frontend/src/pages/Notifications.tsx b/src/frontend/src/pages/Notifications.tsx index f978a5a75384..189040c03557 100644 --- a/src/frontend/src/pages/Notifications.tsx +++ b/src/frontend/src/pages/Notifications.tsx @@ -12,15 +12,19 @@ import { useCallback, useMemo } from 'react'; import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import { apiUrl } from '@lib/functions/Api'; +import { useNavigate } from 'react-router-dom'; import { ActionButton } from '../components/buttons/ActionButton'; import { PageDetail } from '../components/nav/PageDetail'; import { PanelGroup } from '../components/panels/PanelGroup'; import { useApi } from '../contexts/ApiContext'; import { useTable } from '../hooks/UseTable'; +import { useGlobalSettingsState } from '../states/SettingsState'; import { NotificationTable } from '../tables/notifications/NotificationTable'; export default function NotificationsPage() { const api = useApi(); + const navigate = useNavigate(); + const globalSettings = useGlobalSettingsState(); const unreadTable = useTable('unreadnotifications'); const readTable = useTable('readnotifications'); @@ -125,6 +129,11 @@ export default function NotificationsPage() { ]; }, [unreadTable, readTable]); + if (!globalSettings.isSet('NOTIFICATIONS_ENABLE')) { + // Redirect to the dashboard if notifications are not enabled + navigate('/'); + } + return (