mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
feat: Add email notification when a part is changed (#9275)
* Add function to star / unstar a part * Also use with category * Email notification when a part is changed Fixes #7834 * enable disabling of recent checks * Add error handler * remove unneeded function
This commit is contained in:
parent
8e0f79cdfc
commit
42dcc01f9d
@ -366,6 +366,7 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
||||
target_exclude = kwargs.get('target_exclude')
|
||||
context = kwargs.get('context', {})
|
||||
delivery_methods = kwargs.get('delivery_methods')
|
||||
check_recent = kwargs.get('check_recent', True)
|
||||
|
||||
# Check if data is importing currently
|
||||
if isImportingData() or isRebuildingData():
|
||||
@ -391,7 +392,9 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs):
|
||||
# Check if we have notified recently...
|
||||
delta = timedelta(days=1)
|
||||
|
||||
if common.models.NotificationEntry.check_recent(category, obj_ref_value, delta):
|
||||
if check_recent and common.models.NotificationEntry.check_recent(
|
||||
category, obj_ref_value, delta
|
||||
):
|
||||
logger.info(
|
||||
"Notification '%s' has recently been sent for '%s' - SKIPPING",
|
||||
category,
|
||||
|
@ -0,0 +1,69 @@
|
||||
"""Core set of Notifications as a Plugin."""
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import structlog
|
||||
|
||||
import common.models
|
||||
import common.notifications
|
||||
import InvenTree.helpers
|
||||
import InvenTree.helpers_email
|
||||
import InvenTree.helpers_model
|
||||
import InvenTree.tasks
|
||||
from part.models import Part
|
||||
from plugin import InvenTreePlugin
|
||||
from plugin.mixins import EventMixin, SettingsMixin
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
class PartNotificationsPlugin(SettingsMixin, EventMixin, InvenTreePlugin):
|
||||
"""Core notification methods for InvenTree."""
|
||||
|
||||
NAME = 'PartNotificationsPlugin'
|
||||
TITLE = _('Part Notifications')
|
||||
AUTHOR = _('InvenTree contributors')
|
||||
DESCRIPTION = _('Notify users about part changes')
|
||||
VERSION = '1.0.0'
|
||||
|
||||
SETTINGS = {
|
||||
'ENABLE_PART_NOTIFICATIONS': {
|
||||
'name': _('Send notifications'),
|
||||
'description': _('Send notifications for part changes to subscribed users'),
|
||||
'default': False,
|
||||
'validator': bool,
|
||||
}
|
||||
}
|
||||
|
||||
def wants_process_event(self, event):
|
||||
"""Return whether given event should be processed or not."""
|
||||
return event.startswith('part_part.')
|
||||
|
||||
def process_event(self, event, *args, **kwargs):
|
||||
"""Custom event processing."""
|
||||
if not self.get_setting('ENABLE_PART_NOTIFICATIONS'):
|
||||
return
|
||||
part = Part.objects.get(pk=kwargs['id'])
|
||||
part_action = event.split('.')[-1]
|
||||
|
||||
name = _('Changed part notification')
|
||||
common.notifications.trigger_notification(
|
||||
part,
|
||||
'part.notification',
|
||||
target_fnc=part.get_subscribers,
|
||||
check_recent=False,
|
||||
context={
|
||||
'part': part,
|
||||
'name': name,
|
||||
'message': _(
|
||||
f'The part `{part.name}` has been triggered with a `{part_action}` event'
|
||||
),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(
|
||||
part.get_absolute_url()
|
||||
),
|
||||
'template': {
|
||||
'html': 'email/part_event_notification.html',
|
||||
'subject': name,
|
||||
},
|
||||
},
|
||||
)
|
@ -0,0 +1,33 @@
|
||||
{% extends "email/email.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load inventree_extras %}
|
||||
|
||||
{% block title %}
|
||||
{{ message }}
|
||||
{% if link %}
|
||||
<p>{% trans "Click on the following link to view this part" %}: <a href="{{ link }}">{{ link }}</a></p>
|
||||
{% endif %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block body %}
|
||||
<tr style="height: 3rem; border-bottom: 1px solid">
|
||||
<th>{% trans "Part" %}</th>
|
||||
<th>{% trans "Part Category" %}</th>
|
||||
<th>{% trans "Total Stock" %}</th>
|
||||
<th>{% trans "Available" %}</th>
|
||||
<th>{% trans "Minimum Quantity" %}</th>
|
||||
</tr>
|
||||
|
||||
<tr style="height: 3rem">
|
||||
<td style="text-align: center;">{{ part.full_name }}</td>
|
||||
<td style="text-align: center;">{{ part.category_path }}</td>
|
||||
<td style="text-align: center;">{% decimal part.total_stock %}</td>
|
||||
<td style="text-align: center;">{% decimal part.available_stock %}</td>
|
||||
<td style="text-align: center;">{% decimal part.minimum_stock %}</td>
|
||||
</tr>
|
||||
{% endblock body %}
|
||||
|
||||
{% block footer_prefix %}
|
||||
<p><em>{% blocktrans with part=part.name %}You are receiving this email because you are subscribed to notifications for this part or a category that it is part of {% endblocktrans %}.</em></p>
|
||||
{% endblock footer_prefix %}
|
62
src/frontend/src/components/buttons/StarredToggleButton.tsx
Normal file
62
src/frontend/src/components/buttons/StarredToggleButton.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconBell } from '@tabler/icons-react';
|
||||
import { useApi } from '../../contexts/ApiContext';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { ModelType } from '../../enums/ModelType';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { ActionButton } from './ActionButton';
|
||||
|
||||
export default function StarredToggleButton({
|
||||
instance,
|
||||
model,
|
||||
successFunction
|
||||
}: Readonly<{
|
||||
instance: any;
|
||||
model: ModelType.part | ModelType.partcategory;
|
||||
successFunction: () => void;
|
||||
}>): JSX.Element {
|
||||
const api = useApi();
|
||||
|
||||
function change(starred: boolean, partPk: number) {
|
||||
api
|
||||
.patch(
|
||||
apiUrl(
|
||||
model == ModelType.part
|
||||
? ApiEndpoints.part_list
|
||||
: ApiEndpoints.category_list,
|
||||
partPk
|
||||
),
|
||||
{ starred: !starred }
|
||||
)
|
||||
.then(() => {
|
||||
showNotification({
|
||||
title: 'Subscription updated',
|
||||
message: `Subscription ${starred ? 'removed' : 'added'}`,
|
||||
autoClose: 5000,
|
||||
color: 'blue'
|
||||
});
|
||||
successFunction();
|
||||
})
|
||||
.catch((error) => {
|
||||
showNotification({
|
||||
title: 'Error',
|
||||
message: error.message,
|
||||
autoClose: 5000,
|
||||
color: 'red'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
icon={<IconBell />}
|
||||
color={instance.starred ? 'green' : 'blue'}
|
||||
size='lg'
|
||||
variant={instance.starred ? 'filled' : 'outline'}
|
||||
tooltip={t`Unsubscribe from part`}
|
||||
onClick={() => change(instance.starred, instance.pk)}
|
||||
tooltipAlignment='bottom'
|
||||
/>
|
||||
);
|
||||
}
|
@ -11,6 +11,7 @@ import { useMemo, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
@ -227,6 +228,14 @@ export default function CategoryDetail() {
|
||||
model={ModelType.partcategory}
|
||||
id={category.pk}
|
||||
/>,
|
||||
<StarredToggleButton
|
||||
key='starred_change'
|
||||
instance={category}
|
||||
model={ModelType.partcategory}
|
||||
successFunction={() => {
|
||||
refreshInstance();
|
||||
}}
|
||||
/>,
|
||||
<OptionsActionDropdown
|
||||
key='category-actions'
|
||||
tooltip={t`Category Actions`}
|
||||
@ -244,7 +253,7 @@ export default function CategoryDetail() {
|
||||
]}
|
||||
/>
|
||||
];
|
||||
}, [id, user, category.pk]);
|
||||
}, [id, user, category.pk, category.starred]);
|
||||
|
||||
const panels: PanelType[] = useMemo(
|
||||
() => [
|
||||
|
@ -35,6 +35,7 @@ import Select from 'react-select';
|
||||
|
||||
import AdminButton from '../../components/buttons/AdminButton';
|
||||
import { PrintingActions } from '../../components/buttons/PrintingActions';
|
||||
import StarredToggleButton from '../../components/buttons/StarredToggleButton';
|
||||
import {
|
||||
type DetailsField,
|
||||
DetailsTable
|
||||
@ -881,6 +882,14 @@ export default function PartDetail() {
|
||||
const partActions = useMemo(() => {
|
||||
return [
|
||||
<AdminButton model={ModelType.part} id={part.pk} />,
|
||||
<StarredToggleButton
|
||||
key='starred_change'
|
||||
instance={part}
|
||||
model={ModelType.part}
|
||||
successFunction={() => {
|
||||
refreshInstance();
|
||||
}}
|
||||
/>,
|
||||
<BarcodeActionDropdown
|
||||
model={ModelType.part}
|
||||
pk={part.pk}
|
||||
|
Loading…
x
Reference in New Issue
Block a user