mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 03:26:45 +00:00
Notify subscribed users (#9446)
* Update docstrings * Update 'notify_responsible' function * Update build notifications * Update notifications for external orders * Update more notifications * Notify for overdue return orders * Fix typos * Fixes
This commit is contained in:
parent
67bdf3162a
commit
9c419e6ac1
@ -265,6 +265,7 @@ def notify_responsible(
|
||||
sender,
|
||||
content: NotificationBody = InvenTreeNotificationBodies.NewOrder,
|
||||
exclude=None,
|
||||
extra_users: Optional[list] = None,
|
||||
):
|
||||
"""Notify all responsible parties of a change in an instance.
|
||||
|
||||
@ -276,15 +277,19 @@ def notify_responsible(
|
||||
sender: Sender model reference
|
||||
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
|
||||
exclude (User, optional): User instance that should be excluded. Defaults to None.
|
||||
extra_users (list, optional): List of extra users to notify. Defaults to None.
|
||||
"""
|
||||
import InvenTree.ready
|
||||
|
||||
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
|
||||
return
|
||||
|
||||
notify_users(
|
||||
[instance.responsible], instance, sender, content=content, exclude=exclude
|
||||
)
|
||||
users = [instance.responsible]
|
||||
|
||||
if extra_users:
|
||||
users.extend(extra_users)
|
||||
|
||||
notify_users(users, instance, sender, content=content, exclude=exclude)
|
||||
|
||||
|
||||
def notify_users(
|
||||
|
@ -628,8 +628,13 @@ class Build(
|
||||
self.allocated_stock.delete()
|
||||
|
||||
@transaction.atomic
|
||||
def complete_build(self, user, trim_allocated_stock=False):
|
||||
"""Mark this build as complete."""
|
||||
def complete_build(self, user: User, trim_allocated_stock: bool = False):
|
||||
"""Mark this build as complete.
|
||||
|
||||
Arguments:
|
||||
user: The user who is completing the build
|
||||
trim_allocated_stock: If True, trim any allocated stock
|
||||
"""
|
||||
return self.handle_transition(
|
||||
self.status,
|
||||
BuildStatus.COMPLETE.value,
|
||||
@ -681,6 +686,9 @@ class Build(
|
||||
# Notify users that this build has been completed
|
||||
targets = [self.issued_by, self.responsible]
|
||||
|
||||
# Also inform anyone subscribed to the assembly part
|
||||
targets.extend(self.part.get_subscribers())
|
||||
|
||||
# Notify those users interested in the parent build
|
||||
if self.parent:
|
||||
targets.append(self.parent.issued_by)
|
||||
@ -817,6 +825,7 @@ class Build(
|
||||
Build,
|
||||
exclude=self.issued_by,
|
||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||
extra_users=self.part.get_subscribers(),
|
||||
)
|
||||
|
||||
trigger_event(BuildEvents.CANCELLED, id=self.pk)
|
||||
@ -1470,7 +1479,10 @@ def after_save_build(sender, instance: Build, created: bool, **kwargs):
|
||||
|
||||
# Notify the responsible users that the build order has been created
|
||||
InvenTree.helpers_model.notify_responsible(
|
||||
instance, sender, exclude=instance.issued_by
|
||||
instance,
|
||||
sender,
|
||||
exclude=instance.issued_by,
|
||||
extra_users=instance.part.get_subscribers(),
|
||||
)
|
||||
|
||||
else:
|
||||
|
@ -262,6 +262,8 @@ def notify_overdue_build_order(bo: build_models.Build):
|
||||
if bo.responsible:
|
||||
targets.append(bo.responsible)
|
||||
|
||||
targets.extend(bo.part.get_subscribers())
|
||||
|
||||
name = _('Overdue Build Order')
|
||||
|
||||
context = {
|
||||
|
@ -575,6 +575,21 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
"""Return the associated barcode model type code for this model."""
|
||||
return 'PO'
|
||||
|
||||
def subscribed_users(self) -> list[User]:
|
||||
"""Return a list of users subscribed to this PurchaseOrder.
|
||||
|
||||
By this, we mean users to are interested in any of the parts associated with this order.
|
||||
"""
|
||||
subscribed_users = set()
|
||||
|
||||
for line in self.lines.all():
|
||||
if line.part and line.part.part:
|
||||
# Add the part to the list of subscribed users
|
||||
for user in line.part.part.get_subscribers():
|
||||
subscribed_users.add(user)
|
||||
|
||||
return list(subscribed_users)
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this PurchaseOrder."""
|
||||
return f'{self.reference} - {self.supplier.name if self.supplier else _("deleted")}'
|
||||
@ -747,6 +762,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
PurchaseOrder,
|
||||
exclude=self.created_by,
|
||||
content=InvenTreeNotificationBodies.NewOrder,
|
||||
extra_users=self.subscribed_users(),
|
||||
)
|
||||
|
||||
def _action_complete(self, *args, **kwargs):
|
||||
@ -841,6 +857,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
PurchaseOrder,
|
||||
exclude=self.created_by,
|
||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||
extra_users=self.subscribed_users(),
|
||||
)
|
||||
|
||||
@property
|
||||
@ -1045,6 +1062,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
||||
PurchaseOrder,
|
||||
exclude=user,
|
||||
content=InvenTreeNotificationBodies.ItemsReceived,
|
||||
extra_users=line.part.part.get_subscribers(),
|
||||
)
|
||||
|
||||
|
||||
@ -1096,6 +1114,21 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
"""Return the associated barcode model type code for this model."""
|
||||
return 'SO'
|
||||
|
||||
def subscribed_users(self) -> list[User]:
|
||||
"""Return a list of users subscribed to this SalesOrder.
|
||||
|
||||
By this, we mean users to are interested in any of the parts associated with this order.
|
||||
"""
|
||||
subscribed_users = set()
|
||||
|
||||
for line in self.lines.all():
|
||||
if line.part:
|
||||
# Add the part to the list of subscribed users
|
||||
for user in line.part.get_subscribers():
|
||||
subscribed_users.add(user)
|
||||
|
||||
return list(subscribed_users)
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this SalesOrder."""
|
||||
return f'{self.reference} - {self.customer.name if self.customer else _("deleted")}'
|
||||
@ -1248,6 +1281,15 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
|
||||
trigger_event(SalesOrderEvents.ISSUED, id=self.pk)
|
||||
|
||||
# Notify users that the order has been placed
|
||||
notify_responsible(
|
||||
self,
|
||||
SalesOrder,
|
||||
exclude=self.created_by,
|
||||
content=InvenTreeNotificationBodies.NewOrder,
|
||||
extra_users=self.subscribed_users(),
|
||||
)
|
||||
|
||||
@property
|
||||
def can_hold(self):
|
||||
"""Return True if this order can be placed on hold."""
|
||||
@ -1325,6 +1367,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
||||
SalesOrder,
|
||||
exclude=self.created_by,
|
||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||
extra_users=self.subscribed_users(),
|
||||
)
|
||||
|
||||
return True
|
||||
@ -1466,9 +1509,6 @@ def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs
|
||||
# Create default shipment
|
||||
SalesOrderShipment.objects.create(order=instance, reference='1')
|
||||
|
||||
# Notify the responsible users that the sales order has been created
|
||||
notify_responsible(instance, sender, exclude=instance.created_by)
|
||||
|
||||
|
||||
class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
|
||||
"""Abstract model for an order line item.
|
||||
@ -2337,6 +2377,21 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
"""Return the associated barcode model type code for this model."""
|
||||
return 'RO'
|
||||
|
||||
def subscribed_users(self) -> list[User]:
|
||||
"""Return a list of users subscribed to this ReturnOrder.
|
||||
|
||||
By this, we mean users to are interested in any of the parts associated with this order.
|
||||
"""
|
||||
subscribed_users = set()
|
||||
|
||||
for line in self.lines.all():
|
||||
if line.item and line.item.part:
|
||||
# Add the part to the list of subscribed users
|
||||
for user in line.item.part.get_subscribers():
|
||||
subscribed_users.add(user)
|
||||
|
||||
return list(subscribed_users)
|
||||
|
||||
def __str__(self):
|
||||
"""Render a string representation of this ReturnOrder."""
|
||||
return f'{self.reference} - {self.customer.name if self.customer else _("no customer")}'
|
||||
@ -2439,6 +2494,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
ReturnOrder,
|
||||
exclude=self.created_by,
|
||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||
extra_users=self.subscribed_users(),
|
||||
)
|
||||
|
||||
def _action_complete(self, *args, **kwargs):
|
||||
@ -2471,6 +2527,15 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
|
||||
trigger_event(ReturnOrderEvents.ISSUED, id=self.pk)
|
||||
|
||||
# Notify users that the order has been placed
|
||||
notify_responsible(
|
||||
self,
|
||||
ReturnOrder,
|
||||
exclude=self.created_by,
|
||||
content=InvenTreeNotificationBodies.NewOrder,
|
||||
extra_users=self.subscribed_users(),
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def hold_order(self):
|
||||
"""Attempt to tranasition to ON_HOLD status."""
|
||||
@ -2575,6 +2640,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
||||
ReturnOrder,
|
||||
exclude=user,
|
||||
content=InvenTreeNotificationBodies.ReturnOrderItemsReceived,
|
||||
extra_users=line.item.part.get_subscribers(),
|
||||
)
|
||||
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -14,15 +14,24 @@ import InvenTree.helpers_model
|
||||
import order.models
|
||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||
from order.events import PurchaseOrderEvents, SalesOrderEvents
|
||||
from order.status_codes import PurchaseOrderStatusGroups, SalesOrderStatusGroups
|
||||
from order.status_codes import (
|
||||
PurchaseOrderStatusGroups,
|
||||
ReturnOrderStatusGroups,
|
||||
SalesOrderStatusGroups,
|
||||
)
|
||||
from plugin.events import trigger_event
|
||||
from users.models import Owner
|
||||
|
||||
logger = structlog.get_logger('inventree')
|
||||
|
||||
|
||||
def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
||||
"""Notify users that a PurchaseOrder has just become 'overdue'."""
|
||||
targets = []
|
||||
def notify_overdue_purchase_order(po: order.models.PurchaseOrder) -> None:
|
||||
"""Notify users that a PurchaseOrder has just become 'overdue'.
|
||||
|
||||
Arguments:
|
||||
po: The PurchaseOrder object that is overdue.
|
||||
"""
|
||||
targets: list[User | Group | Owner] = []
|
||||
|
||||
if po.created_by:
|
||||
targets.append(po.created_by)
|
||||
@ -30,6 +39,8 @@ def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
||||
if po.responsible:
|
||||
targets.append(po.responsible)
|
||||
|
||||
targets.extend(po.subscribed_users())
|
||||
|
||||
name = _('Overdue Purchase Order')
|
||||
|
||||
context = {
|
||||
@ -86,9 +97,9 @@ def check_overdue_purchase_orders():
|
||||
notified_orders.add(line.order.pk)
|
||||
|
||||
|
||||
def notify_overdue_sales_order(so: order.models.SalesOrder):
|
||||
def notify_overdue_sales_order(so: order.models.SalesOrder) -> None:
|
||||
"""Notify appropriate users that a SalesOrder has just become 'overdue'."""
|
||||
targets = []
|
||||
targets: list[User, Group, Owner] = []
|
||||
|
||||
if so.created_by:
|
||||
targets.append(so.created_by)
|
||||
@ -96,6 +107,8 @@ def notify_overdue_sales_order(so: order.models.SalesOrder):
|
||||
if so.responsible:
|
||||
targets.append(so.responsible)
|
||||
|
||||
targets.extend(so.subscribed_users())
|
||||
|
||||
name = _('Overdue Sales Order')
|
||||
|
||||
context = {
|
||||
@ -149,6 +162,71 @@ def check_overdue_sales_orders():
|
||||
notified_orders.add(line.order.pk)
|
||||
|
||||
|
||||
def notify_overdue_return_order(ro: order.models.ReturnOrder) -> None:
|
||||
"""Notify appropriate users that a ReturnOrder has just become 'overdue'."""
|
||||
targets: list[User, Group, Owner] = []
|
||||
|
||||
if ro.created_by:
|
||||
targets.append(ro.created_by)
|
||||
|
||||
if ro.responsible:
|
||||
targets.append(ro.responsible)
|
||||
|
||||
targets.extend(ro.subscribed_users())
|
||||
|
||||
name = _('Overdue Return Order')
|
||||
|
||||
context = {
|
||||
'order': ro,
|
||||
'name': name,
|
||||
'message': _(f'Return order {ro} is now overdue'),
|
||||
'link': InvenTree.helpers_model.construct_absolute_url(ro.get_absolute_url()),
|
||||
'template': {'html': 'email/overdue_return_order.html', 'subject': name},
|
||||
}
|
||||
|
||||
event_name = SalesOrderEvents.OVERDUE
|
||||
|
||||
# Send a notification to the appropriate users
|
||||
common.notifications.trigger_notification(
|
||||
ro, event_name, targets=targets, context=context
|
||||
)
|
||||
|
||||
# Register a matching event to the plugin system
|
||||
trigger_event(event_name, return_order=ro.pk)
|
||||
|
||||
|
||||
@scheduled_task(ScheduledTask.DAILY)
|
||||
def check_overdue_return_orders():
|
||||
"""Check if any outstanding return orders have just become overdue.
|
||||
|
||||
- This check is performed daily
|
||||
- Look at the 'target_date' of any outstanding return order objects
|
||||
- If the 'target_date' expired *yesterday* then the order is just out of date
|
||||
"""
|
||||
yesterday = datetime.now().date() - timedelta(days=1)
|
||||
|
||||
overdue_orders = order.models.ReturnOrder.objects.filter(
|
||||
target_date=yesterday, status__in=ReturnOrderStatusGroups.OPEN
|
||||
)
|
||||
|
||||
overdue_lines = order.models.ReturnOrderLineItem.objects.filter(
|
||||
target_date=yesterday,
|
||||
order__status__in=ReturnOrderStatusGroups.OPEN,
|
||||
received_date__isnull=True,
|
||||
)
|
||||
|
||||
notified_orders = set()
|
||||
|
||||
for ro in overdue_orders:
|
||||
notify_overdue_return_order(ro)
|
||||
notified_orders.add(ro.pk)
|
||||
|
||||
for line in overdue_lines:
|
||||
if line.order.pk not in notified_orders:
|
||||
notify_overdue_return_order(line.order)
|
||||
notified_orders.add(line.order.pk)
|
||||
|
||||
|
||||
def complete_sales_order_shipment(shipment_id: int, user_id: int) -> None:
|
||||
"""Complete allocations for a pending shipment against a SalesOrder.
|
||||
|
||||
|
@ -333,18 +333,20 @@ class SalesOrderTest(TestCase):
|
||||
self.assertEqual(len(messages), 1)
|
||||
|
||||
def test_new_so_notification(self):
|
||||
"""Test that a notification is sent when a new SalesOrder is created.
|
||||
"""Test that a notification is sent when a new SalesOrder is issued.
|
||||
|
||||
- The responsible user should receive a notification
|
||||
- The creating user should *not* receive a notification
|
||||
"""
|
||||
SalesOrder.objects.create(
|
||||
so = 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)),
|
||||
)
|
||||
|
||||
so.issue_order()
|
||||
|
||||
messages = NotificationMessage.objects.filter(category='order.new_salesorder')
|
||||
|
||||
# A notification should have been generated for user 4 (who is a member of group 3)
|
||||
|
@ -286,8 +286,15 @@ class PartCategory(InvenTree.models.InvenTreeTree):
|
||||
|
||||
return prefetch.filter(category=self.id)
|
||||
|
||||
def get_subscribers(self, include_parents=True):
|
||||
"""Return a list of users who subscribe to this PartCategory."""
|
||||
def get_subscribers(self, include_parents: bool = True) -> list[User]:
|
||||
"""Return a list of users who subscribe to this PartCategory.
|
||||
|
||||
Arguments:
|
||||
include_parents (bool): If True, include users who subscribe to parent categories.
|
||||
|
||||
Returns:
|
||||
list[User]: List of users who subscribe to this category.
|
||||
"""
|
||||
subscribers = set()
|
||||
|
||||
if include_parents:
|
||||
@ -1429,9 +1436,18 @@ class Part(
|
||||
"""
|
||||
return self.total_stock - self.allocation_count() + self.on_order
|
||||
|
||||
def get_subscribers(self, include_variants=True, include_categories=True):
|
||||
def get_subscribers(
|
||||
self, include_variants: bool = True, include_categories: bool = True
|
||||
) -> list[User]:
|
||||
"""Return a list of users who are 'subscribed' to this part.
|
||||
|
||||
Arguments:
|
||||
include_variants: If True, include users who are subscribed to a variant part
|
||||
include_categories: If True, include users who are subscribed to the category
|
||||
|
||||
Returns:
|
||||
list[User]: A list of users who are subscribed to this part
|
||||
|
||||
A user may 'subscribe' to this part in the following ways:
|
||||
|
||||
a) Subscribing to the part instance directly
|
||||
|
@ -0,0 +1,24 @@
|
||||
{% 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 %}
|
||||
|
||||
{% block body %}
|
||||
<tr style="height: 3rem; border-bottom: 1px solid">
|
||||
<th>{% trans "Return Order" %}</th>
|
||||
<th>{% trans "Customer" %}</th>
|
||||
</tr>
|
||||
|
||||
<tr style="height: 3rem">
|
||||
<td style="text-align: center;">{{ order }}</td>
|
||||
<td style="text-align: center;">{{ order.customer }}</td>
|
||||
</tr>
|
||||
|
||||
{% endblock body %}
|
Loading…
x
Reference in New Issue
Block a user