mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-30 20:55:42 +00:00 
			
		
		
		
	Notification on new orders (#3145)
* Trigger a notification when a new SalesOrder is created - Notify the "responsible" owners (excluding the creator) - Add unit test for new notification * Adds notification when a new PurchaseOrder is created * Add notification when a new build order is created - Includes unit tests * Refactor order notification code - Adds a "exclude users" option for sending notifications * Fixes for notification refactoring * make notification a helper * reduce statements togehter * make reuse easier * Add docs * Make context variables clearer * fix assertation * Fix set notation Co-authored-by: Matthias <code@mjmair.com>
This commit is contained in:
		| @@ -18,6 +18,8 @@ from PIL import Image | |||||||
|  |  | ||||||
| import InvenTree.version | import InvenTree.version | ||||||
| from common.models import InvenTreeSetting | from common.models import InvenTreeSetting | ||||||
|  | from common.notifications import (InvenTreeNotificationBodies, | ||||||
|  |                                   NotificationBody, trigger_notification) | ||||||
| from common.settings import currency_code_default | from common.settings import currency_code_default | ||||||
|  |  | ||||||
| from .api_tester import UserMixin | from .api_tester import UserMixin | ||||||
| @@ -719,3 +721,46 @@ def inheritors(cls): | |||||||
| class InvenTreeTestCase(UserMixin, TestCase): | class InvenTreeTestCase(UserMixin, TestCase): | ||||||
|     """Testcase with user setup buildin.""" |     """Testcase with user setup buildin.""" | ||||||
|     pass |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def notify_responsible(instance, sender, content: NotificationBody = InvenTreeNotificationBodies.NewOrder, exclude=None): | ||||||
|  |     """Notify all responsible parties of a change in an instance. | ||||||
|  |  | ||||||
|  |     Parses the supplied content with the provided instance and sender and sends a notification to all responsible users, | ||||||
|  |     excluding the optional excluded list. | ||||||
|  |  | ||||||
|  |     Args: | ||||||
|  |         instance: The newly created instance | ||||||
|  |         sender: Sender model reference | ||||||
|  |         content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder. | ||||||
|  |         exclude (User, optional): User instance that should be excluded. Defaults to None. | ||||||
|  |     """ | ||||||
|  |     if instance.responsible is not None: | ||||||
|  |         # Setup context for notification parsing | ||||||
|  |         content_context = { | ||||||
|  |             'instance': str(instance), | ||||||
|  |             'verbose_name': sender._meta.verbose_name, | ||||||
|  |             'app_label': sender._meta.app_label, | ||||||
|  |             'model_name': sender._meta.model_name, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         # Setup notification context | ||||||
|  |         context = { | ||||||
|  |             'instance': instance, | ||||||
|  |             'name': content.name.format(**content_context), | ||||||
|  |             'message': content.message.format(**content_context), | ||||||
|  |             'link': InvenTree.helpers.construct_absolute_url(instance.get_absolute_url()), | ||||||
|  |             'template': { | ||||||
|  |                 'html': content.template.format(**content_context), | ||||||
|  |                 'subject': content.name.format(**content_context), | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         # Create notification | ||||||
|  |         trigger_notification( | ||||||
|  |             instance, | ||||||
|  |             content.slug.format(**content_context), | ||||||
|  |             targets=[instance.responsible], | ||||||
|  |             target_exclude=[exclude], | ||||||
|  |             context=context, | ||||||
|  |         ) | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ from mptt.exceptions import InvalidMove | |||||||
| from rest_framework import serializers | from rest_framework import serializers | ||||||
|  |  | ||||||
| from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode | from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode | ||||||
| from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode | from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode, notify_responsible | ||||||
| from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin | from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin | ||||||
| from InvenTree.validators import validate_build_order_reference | from InvenTree.validators import validate_build_order_reference | ||||||
|  |  | ||||||
| @@ -1049,6 +1049,9 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs): | |||||||
|         # Run checks on required parts |         # Run checks on required parts | ||||||
|         InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance) |         InvenTree.tasks.offload_task(build_tasks.check_build_stock, instance) | ||||||
|  |  | ||||||
|  |         # Notify the responsible users that the build order has been created | ||||||
|  |         notify_responsible(instance, sender, exclude=instance.issued_by) | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildOrderAttachment(InvenTreeAttachment): | class BuildOrderAttachment(InvenTreeAttachment): | ||||||
|     """Model for storing file attachments against a BuildOrder object.""" |     """Model for storing file attachments against a BuildOrder object.""" | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ from datetime import datetime, timedelta | |||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
|  |  | ||||||
| from django.contrib.auth import get_user_model | from django.contrib.auth import get_user_model | ||||||
|  | from django.contrib.auth.models import Group | ||||||
| from django.core.exceptions import ValidationError | from django.core.exceptions import ValidationError | ||||||
|  |  | ||||||
| from InvenTree import status_codes as status | from InvenTree import status_codes as status | ||||||
| @@ -14,6 +15,7 @@ import build.tasks | |||||||
| from build.models import Build, BuildItem, get_next_build_number | from build.models import Build, BuildItem, get_next_build_number | ||||||
| from part.models import Part, BomItem, BomItemSubstitute | from part.models import Part, BomItem, BomItemSubstitute | ||||||
| from stock.models import StockItem | from stock.models import StockItem | ||||||
|  | from users.models import Owner | ||||||
|  |  | ||||||
|  |  | ||||||
| class BuildTestBase(TestCase): | class BuildTestBase(TestCase): | ||||||
| @@ -382,6 +384,46 @@ class BuildTest(BuildTestBase): | |||||||
|         for output in outputs: |         for output in outputs: | ||||||
|             self.assertFalse(output.is_building) |             self.assertFalse(output.is_building) | ||||||
|  |  | ||||||
|  |     def test_overdue_notification(self): | ||||||
|  |         """Test sending of notifications when a build order is overdue.""" | ||||||
|  |  | ||||||
|  |         self.build.target_date = datetime.now().date() - timedelta(days=1) | ||||||
|  |         self.build.save() | ||||||
|  |  | ||||||
|  |         # Check for overdue orders | ||||||
|  |         build.tasks.check_overdue_build_orders() | ||||||
|  |  | ||||||
|  |         message = common.models.NotificationMessage.objects.get( | ||||||
|  |             category='build.overdue_build_order', | ||||||
|  |             user__id=1, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(message.name, 'Overdue Build Order') | ||||||
|  |  | ||||||
|  |     def test_new_build_notification(self): | ||||||
|  |         """Test that a notification is sent when a new build is created""" | ||||||
|  |  | ||||||
|  |         Build.objects.create( | ||||||
|  |             reference='IIIII', | ||||||
|  |             title='Some new build', | ||||||
|  |             part=self.assembly, | ||||||
|  |             quantity=5, | ||||||
|  |             issued_by=get_user_model().objects.get(pk=2), | ||||||
|  |             responsible=Owner.create(obj=Group.objects.get(pk=3)) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Two notifications should have been sent | ||||||
|  |         messages = common.models.NotificationMessage.objects.filter( | ||||||
|  |             category='build.new_build', | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(messages.count(), 2) | ||||||
|  |  | ||||||
|  |         self.assertFalse(messages.filter(user__pk=2).exists()) | ||||||
|  |  | ||||||
|  |         self.assertTrue(messages.filter(user__pk=3).exists()) | ||||||
|  |         self.assertTrue(messages.filter(user__pk=4).exists()) | ||||||
|  |  | ||||||
|  |  | ||||||
| class AutoAllocationTests(BuildTestBase): | class AutoAllocationTests(BuildTestBase): | ||||||
|     """Tests for auto allocating stock against a build order""" |     """Tests for auto allocating stock against a build order""" | ||||||
| @@ -479,19 +521,3 @@ class AutoAllocationTests(BuildTestBase): | |||||||
|  |  | ||||||
|         self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0) |         self.assertEqual(self.build.unallocated_quantity(self.bom_item_1), 0) | ||||||
|         self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 0) |         self.assertEqual(self.build.unallocated_quantity(self.bom_item_2), 0) | ||||||
|  |  | ||||||
|     def test_overdue_notification(self): |  | ||||||
|         """Test sending of notifications when a build order is overdue.""" |  | ||||||
|  |  | ||||||
|         self.build.target_date = datetime.now().date() - timedelta(days=1) |  | ||||||
|         self.build.save() |  | ||||||
|  |  | ||||||
|         # Check for overdue orders |  | ||||||
|         build.tasks.check_overdue_build_orders() |  | ||||||
|  |  | ||||||
|         message = common.models.NotificationMessage.objects.get( |  | ||||||
|             category='build.overdue_build_order', |  | ||||||
|             user__id=1, |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|         self.assertEqual(message.name, 'Overdue Build Order') |  | ||||||
|   | |||||||
| @@ -1,13 +1,15 @@ | |||||||
| """Base classes and functions for notifications.""" | """Base classes and functions for notifications.""" | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
|  | from dataclasses import dataclass | ||||||
| from datetime import timedelta | from datetime import timedelta | ||||||
|  |  | ||||||
| from django.contrib.auth import get_user_model | from django.contrib.auth import get_user_model | ||||||
| from django.contrib.auth.models import Group | from django.contrib.auth.models import Group | ||||||
|  | from django.utils.translation import gettext_lazy as _ | ||||||
|  |  | ||||||
|  | import InvenTree.helpers | ||||||
| from common.models import NotificationEntry, NotificationMessage | from common.models import NotificationEntry, NotificationMessage | ||||||
| from InvenTree.helpers import inheritors |  | ||||||
| from InvenTree.ready import isImportingData | from InvenTree.ready import isImportingData | ||||||
| from plugin import registry | from plugin import registry | ||||||
| from plugin.models import NotificationUserSetting | from plugin.models import NotificationUserSetting | ||||||
| @@ -179,7 +181,7 @@ class MethodStorageClass: | |||||||
|             selected_classes (class, optional): References to the classes that should be registered. Defaults to None. |             selected_classes (class, optional): References to the classes that should be registered. Defaults to None. | ||||||
|         """ |         """ | ||||||
|         logger.info('collecting notification methods') |         logger.info('collecting notification methods') | ||||||
|         current_method = inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS |         current_method = InvenTree.helpers.inheritors(NotificationMethod) - IGNORED_NOTIFICATION_CLS | ||||||
|  |  | ||||||
|         # for testing selective loading is made available |         # for testing selective loading is made available | ||||||
|         if selected_classes: |         if selected_classes: | ||||||
| @@ -257,12 +259,51 @@ class UIMessageNotification(SingleNotificationMethod): | |||||||
|         return True |         return True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @dataclass() | ||||||
|  | class NotificationBody: | ||||||
|  |     """Information needed to create a notification. | ||||||
|  |  | ||||||
|  |     Attributes: | ||||||
|  |         name (str): Name (or subject) of the notification | ||||||
|  |         slug (str): Slugified reference for notification | ||||||
|  |         message (str): Notification message as text. Should not be longer than 120 chars. | ||||||
|  |         template (str): Reference to the html template for the notification. | ||||||
|  |  | ||||||
|  |     The strings support f-string sytle fomratting with context variables parsed at runtime. | ||||||
|  |  | ||||||
|  |     Context variables: | ||||||
|  |         instance: Text representing the instance | ||||||
|  |         verbose_name: Verbose name of the model | ||||||
|  |         app_label: App label (slugified) of the model | ||||||
|  |         model_name': Name (slugified) of the model | ||||||
|  |     """ | ||||||
|  |     name: str | ||||||
|  |     slug: str | ||||||
|  |     message: str | ||||||
|  |     template: str | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class InvenTreeNotificationBodies: | ||||||
|  |     """Default set of notifications for InvenTree. | ||||||
|  |  | ||||||
|  |     Contains regularly used notification bodies. | ||||||
|  |     """ | ||||||
|  |     NewOrder = NotificationBody( | ||||||
|  |         name=_("New {verbose_name}"), | ||||||
|  |         slug='{app_label}.new_{model_name}', | ||||||
|  |         message=_("A new {verbose_name} has been created and ,assigned to you"), | ||||||
|  |         template='email/new_order_assigned.html', | ||||||
|  |     ) | ||||||
|  |     """Send when a new order (build, sale or purchase) was created.""" | ||||||
|  |  | ||||||
|  |  | ||||||
| def trigger_notification(obj, category=None, obj_ref='pk', **kwargs): | def trigger_notification(obj, category=None, obj_ref='pk', **kwargs): | ||||||
|     """Send out a notification.""" |     """Send out a notification.""" | ||||||
|     targets = kwargs.get('targets', None) |     targets = kwargs.get('targets', None) | ||||||
|     target_fnc = kwargs.get('target_fnc', None) |     target_fnc = kwargs.get('target_fnc', None) | ||||||
|     target_args = kwargs.get('target_args', []) |     target_args = kwargs.get('target_args', []) | ||||||
|     target_kwargs = kwargs.get('target_kwargs', {}) |     target_kwargs = kwargs.get('target_kwargs', {}) | ||||||
|  |     target_exclude = kwargs.get('target_exclude', None) | ||||||
|     context = kwargs.get('context', {}) |     context = kwargs.get('context', {}) | ||||||
|     delivery_methods = kwargs.get('delivery_methods', None) |     delivery_methods = kwargs.get('delivery_methods', None) | ||||||
|  |  | ||||||
| @@ -290,6 +331,9 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs): | |||||||
|  |  | ||||||
|     logger.info(f"Gathering users for notification '{category}'") |     logger.info(f"Gathering users for notification '{category}'") | ||||||
|  |  | ||||||
|  |     if target_exclude is None: | ||||||
|  |         target_exclude = set() | ||||||
|  |  | ||||||
|     # Collect possible targets |     # Collect possible targets | ||||||
|     if not targets: |     if not targets: | ||||||
|         targets = target_fnc(*target_args, **target_kwargs) |         targets = target_fnc(*target_args, **target_kwargs) | ||||||
| @@ -302,15 +346,19 @@ def trigger_notification(obj, category=None, obj_ref='pk', **kwargs): | |||||||
|         for target in targets: |         for target in targets: | ||||||
|             # User instance is provided |             # User instance is provided | ||||||
|             if isinstance(target, get_user_model()): |             if isinstance(target, get_user_model()): | ||||||
|  |                 if target not in target_exclude: | ||||||
|                     target_users.add(target) |                     target_users.add(target) | ||||||
|             # Group instance is provided |             # Group instance is provided | ||||||
|             elif isinstance(target, Group): |             elif isinstance(target, Group): | ||||||
|                 for user in get_user_model().objects.filter(groups__name=target.name): |                 for user in get_user_model().objects.filter(groups__name=target.name): | ||||||
|  |                     if user not in target_exclude: | ||||||
|                         target_users.add(user) |                         target_users.add(user) | ||||||
|             # Owner instance (either 'user' or 'group' is provided) |             # Owner instance (either 'user' or 'group' is provided) | ||||||
|             elif isinstance(target, Owner): |             elif isinstance(target, Owner): | ||||||
|                 for owner in target.get_related_owners(include_group=False): |                 for owner in target.get_related_owners(include_group=False): | ||||||
|                     target_users.add(owner.owner) |                     user = owner.owner | ||||||
|  |                     if user not in target_exclude: | ||||||
|  |                         target_users.add(user) | ||||||
|             # Unhandled type |             # Unhandled type | ||||||
|             else: |             else: | ||||||
|                 logger.error(f"Unknown target passed to trigger_notification method: {target}") |                 logger.error(f"Unknown target passed to trigger_notification method: {target}") | ||||||
|   | |||||||
| @@ -30,7 +30,8 @@ import InvenTree.ready | |||||||
| from common.settings import currency_code_default | from common.settings import currency_code_default | ||||||
| from company.models import Company, SupplierPart | from company.models import Company, SupplierPart | ||||||
| from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField | from InvenTree.fields import InvenTreeModelMoneyField, RoundingDecimalField | ||||||
| from InvenTree.helpers import decimal2string, getSetting, increment | from InvenTree.helpers import (decimal2string, getSetting, increment, | ||||||
|  |                                notify_responsible) | ||||||
| from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin | from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin | ||||||
| from InvenTree.status_codes import (PurchaseOrderStatus, SalesOrderStatus, | from InvenTree.status_codes import (PurchaseOrderStatus, SalesOrderStatus, | ||||||
|                                     StockHistoryCode, StockStatus) |                                     StockHistoryCode, StockStatus) | ||||||
| @@ -574,6 +575,17 @@ class PurchaseOrder(Order): | |||||||
|             self.complete_order()  # This will save the model |             self.complete_order()  # This will save the model | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @receiver(post_save, sender=PurchaseOrder, dispatch_uid='purchase_order_post_save') | ||||||
|  | def after_save_purchase_order(sender, instance: PurchaseOrder, created: bool, **kwargs): | ||||||
|  |     """Callback function to be executed after a PurchaseOrder is saved.""" | ||||||
|  |     if not InvenTree.ready.canAppAccessDatabase(allow_test=True) or InvenTree.ready.isImportingData(): | ||||||
|  |         return | ||||||
|  |  | ||||||
|  |     if created: | ||||||
|  |         # Notify the responsible users that the purchase order has been created | ||||||
|  |         notify_responsible(instance, sender, exclude=instance.created_by) | ||||||
|  |  | ||||||
|  |  | ||||||
| class SalesOrder(Order): | class SalesOrder(Order): | ||||||
|     """A SalesOrder represents a list of goods shipped outwards to a customer. |     """A SalesOrder represents a list of goods shipped outwards to a customer. | ||||||
|  |  | ||||||
| @@ -839,30 +851,30 @@ class SalesOrder(Order): | |||||||
|         return self.pending_shipments().count() |         return self.pending_shipments().count() | ||||||
|  |  | ||||||
|  |  | ||||||
| @receiver(post_save, sender=SalesOrder, dispatch_uid='build_post_save_log') | @receiver(post_save, sender=SalesOrder, dispatch_uid='sales_order_post_save') | ||||||
| def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs): | def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs): | ||||||
|     """Callback function to be executed after a SalesOrder instance is saved. |     """Callback function to be executed after a SalesOrder is saved. | ||||||
|  |  | ||||||
|     - If the SALESORDER_DEFAULT_SHIPMENT setting is enabled, create a default shipment |     - If the SALESORDER_DEFAULT_SHIPMENT setting is enabled, create a default shipment | ||||||
|     - Ignore if the database is not ready for access |     - Ignore if the database is not ready for access | ||||||
|     - Ignore if data import is active |     - Ignore if data import is active | ||||||
|     """ |     """ | ||||||
|  |     if not InvenTree.ready.canAppAccessDatabase(allow_test=True) or InvenTree.ready.isImportingData(): | ||||||
|     if not InvenTree.ready.canAppAccessDatabase(allow_test=True): |  | ||||||
|         return |         return | ||||||
|  |  | ||||||
|     if InvenTree.ready.isImportingData(): |     if created: | ||||||
|         return |  | ||||||
|  |  | ||||||
|     if created and getSetting('SALESORDER_DEFAULT_SHIPMENT'): |  | ||||||
|         # A new SalesOrder has just been created |         # A new SalesOrder has just been created | ||||||
|  |  | ||||||
|  |         if getSetting('SALESORDER_DEFAULT_SHIPMENT'): | ||||||
|             # Create default shipment |             # Create default shipment | ||||||
|             SalesOrderShipment.objects.create( |             SalesOrderShipment.objects.create( | ||||||
|                 order=instance, |                 order=instance, | ||||||
|                 reference='1', |                 reference='1', | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |         # Notify the responsible users that the sales order has been created | ||||||
|  |         notify_responsible(instance, sender, exclude=instance.created_by) | ||||||
|  |  | ||||||
|  |  | ||||||
| class PurchaseOrderAttachment(InvenTreeAttachment): | class PurchaseOrderAttachment(InvenTreeAttachment): | ||||||
|     """Model for storing file attachments against a PurchaseOrder object.""" |     """Model for storing file attachments against a PurchaseOrder object.""" | ||||||
|   | |||||||
| @@ -260,3 +260,27 @@ class SalesOrderTest(TestCase): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.assertEqual(len(messages), 2) |         self.assertEqual(len(messages), 2) | ||||||
|  |  | ||||||
|  |     def test_new_so_notification(self): | ||||||
|  |         """Test that a notification is sent when a new SalesOrder is created. | ||||||
|  |  | ||||||
|  |         - The responsible user should receive a notification | ||||||
|  |         - The creating user should *not* receive a notification | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         SalesOrder.objects.create( | ||||||
|  |             customer=self.customer, | ||||||
|  |             reference='1234567', | ||||||
|  |             created_by=get_user_model().objects.get(pk=3), | ||||||
|  |             responsible=Owner.create(obj=Group.objects.get(pk=3)) | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         messages = NotificationMessage.objects.filter( | ||||||
|  |             category='order.new_salesorder', | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # A notification should have been generated for user 4 (who is a member of group 3) | ||||||
|  |         self.assertTrue(messages.filter(user__pk=4).exists()) | ||||||
|  |  | ||||||
|  |         # However *no* notification should have been generated for the creating user | ||||||
|  |         self.assertFalse(messages.filter(user__pk=3).exists()) | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ from django.test import TestCase | |||||||
|  |  | ||||||
| import common.models | import common.models | ||||||
| import order.tasks | import order.tasks | ||||||
| from company.models import SupplierPart | from company.models import Company, SupplierPart | ||||||
| from InvenTree.status_codes import PurchaseOrderStatus | from InvenTree.status_codes import PurchaseOrderStatus | ||||||
| from part.models import Part | from part.models import Part | ||||||
| from stock.models import StockLocation | from stock.models import StockLocation | ||||||
| @@ -237,3 +237,29 @@ class OrderTest(TestCase): | |||||||
|  |  | ||||||
|             self.assertEqual(msg.target_object_id, 1) |             self.assertEqual(msg.target_object_id, 1) | ||||||
|             self.assertEqual(msg.name, 'Overdue Purchase Order') |             self.assertEqual(msg.name, 'Overdue Purchase Order') | ||||||
|  |  | ||||||
|  |     def test_new_po_notification(self): | ||||||
|  |         """Test that a notification is sent when a new PurchaseOrder is created | ||||||
|  |  | ||||||
|  |         - The responsible user(s) should receive a notification | ||||||
|  |         - The creating user should *not* receive a notification | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         PurchaseOrder.objects.create( | ||||||
|  |             supplier=Company.objects.get(pk=1), | ||||||
|  |             reference='XYZABC', | ||||||
|  |             created_by=get_user_model().objects.get(pk=3), | ||||||
|  |             responsible=Owner.create(obj=get_user_model().objects.get(pk=4)), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         messages = common.models.NotificationMessage.objects.filter( | ||||||
|  |             category='order.new_purchaseorder', | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         self.assertEqual(messages.count(), 1) | ||||||
|  |  | ||||||
|  |         # A notification should have been generated for user 4 (who is a member of group 3) | ||||||
|  |         self.assertTrue(messages.filter(user__pk=4).exists()) | ||||||
|  |  | ||||||
|  |         # However *no* notification should have been generated for the creating user | ||||||
|  |         self.assertFalse(messages.filter(user__pk=3).exists()) | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								InvenTree/templates/email/new_order_assigned.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								InvenTree/templates/email/new_order_assigned.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | {% extends "email/email.html" %} | ||||||
|  |  | ||||||
|  | {% load i18n %} | ||||||
|  | {% load inventree_extras %} | ||||||
|  |  | ||||||
|  | {% block title %} | ||||||
|  | {{ message }} | ||||||
|  | {% if link %} | ||||||
|  | <p>{% trans "Click on the following link to view this order" %}: <a href="{{ link }}">{{ link }}</a></p> | ||||||
|  | {% endif %} | ||||||
|  | {% endblock title %} | ||||||
		Reference in New Issue
	
	Block a user