mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +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,
|
sender,
|
||||||
content: NotificationBody = InvenTreeNotificationBodies.NewOrder,
|
content: NotificationBody = InvenTreeNotificationBodies.NewOrder,
|
||||||
exclude=None,
|
exclude=None,
|
||||||
|
extra_users: Optional[list] = None,
|
||||||
):
|
):
|
||||||
"""Notify all responsible parties of a change in an instance.
|
"""Notify all responsible parties of a change in an instance.
|
||||||
|
|
||||||
@ -276,15 +277,19 @@ def notify_responsible(
|
|||||||
sender: Sender model reference
|
sender: Sender model reference
|
||||||
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
|
content (NotificationBody, optional): _description_. Defaults to InvenTreeNotificationBodies.NewOrder.
|
||||||
exclude (User, optional): User instance that should be excluded. Defaults to None.
|
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
|
import InvenTree.ready
|
||||||
|
|
||||||
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
|
if InvenTree.ready.isImportingData() or InvenTree.ready.isRunningMigrations():
|
||||||
return
|
return
|
||||||
|
|
||||||
notify_users(
|
users = [instance.responsible]
|
||||||
[instance.responsible], instance, sender, content=content, exclude=exclude
|
|
||||||
)
|
if extra_users:
|
||||||
|
users.extend(extra_users)
|
||||||
|
|
||||||
|
notify_users(users, instance, sender, content=content, exclude=exclude)
|
||||||
|
|
||||||
|
|
||||||
def notify_users(
|
def notify_users(
|
||||||
|
@ -628,8 +628,13 @@ class Build(
|
|||||||
self.allocated_stock.delete()
|
self.allocated_stock.delete()
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def complete_build(self, user, trim_allocated_stock=False):
|
def complete_build(self, user: User, trim_allocated_stock: bool = False):
|
||||||
"""Mark this build as complete."""
|
"""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(
|
return self.handle_transition(
|
||||||
self.status,
|
self.status,
|
||||||
BuildStatus.COMPLETE.value,
|
BuildStatus.COMPLETE.value,
|
||||||
@ -681,6 +686,9 @@ class Build(
|
|||||||
# Notify users that this build has been completed
|
# Notify users that this build has been completed
|
||||||
targets = [self.issued_by, self.responsible]
|
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
|
# Notify those users interested in the parent build
|
||||||
if self.parent:
|
if self.parent:
|
||||||
targets.append(self.parent.issued_by)
|
targets.append(self.parent.issued_by)
|
||||||
@ -817,6 +825,7 @@ class Build(
|
|||||||
Build,
|
Build,
|
||||||
exclude=self.issued_by,
|
exclude=self.issued_by,
|
||||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||||
|
extra_users=self.part.get_subscribers(),
|
||||||
)
|
)
|
||||||
|
|
||||||
trigger_event(BuildEvents.CANCELLED, id=self.pk)
|
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
|
# Notify the responsible users that the build order has been created
|
||||||
InvenTree.helpers_model.notify_responsible(
|
InvenTree.helpers_model.notify_responsible(
|
||||||
instance, sender, exclude=instance.issued_by
|
instance,
|
||||||
|
sender,
|
||||||
|
exclude=instance.issued_by,
|
||||||
|
extra_users=instance.part.get_subscribers(),
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -262,6 +262,8 @@ def notify_overdue_build_order(bo: build_models.Build):
|
|||||||
if bo.responsible:
|
if bo.responsible:
|
||||||
targets.append(bo.responsible)
|
targets.append(bo.responsible)
|
||||||
|
|
||||||
|
targets.extend(bo.part.get_subscribers())
|
||||||
|
|
||||||
name = _('Overdue Build Order')
|
name = _('Overdue Build Order')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
@ -575,6 +575,21 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
"""Return the associated barcode model type code for this model."""
|
"""Return the associated barcode model type code for this model."""
|
||||||
return 'PO'
|
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):
|
def __str__(self):
|
||||||
"""Render a string representation of this PurchaseOrder."""
|
"""Render a string representation of this PurchaseOrder."""
|
||||||
return f'{self.reference} - {self.supplier.name if self.supplier else _("deleted")}'
|
return f'{self.reference} - {self.supplier.name if self.supplier else _("deleted")}'
|
||||||
@ -747,6 +762,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
PurchaseOrder,
|
PurchaseOrder,
|
||||||
exclude=self.created_by,
|
exclude=self.created_by,
|
||||||
content=InvenTreeNotificationBodies.NewOrder,
|
content=InvenTreeNotificationBodies.NewOrder,
|
||||||
|
extra_users=self.subscribed_users(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _action_complete(self, *args, **kwargs):
|
def _action_complete(self, *args, **kwargs):
|
||||||
@ -841,6 +857,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
PurchaseOrder,
|
PurchaseOrder,
|
||||||
exclude=self.created_by,
|
exclude=self.created_by,
|
||||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||||
|
extra_users=self.subscribed_users(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1045,6 +1062,7 @@ class PurchaseOrder(TotalPriceMixin, Order):
|
|||||||
PurchaseOrder,
|
PurchaseOrder,
|
||||||
exclude=user,
|
exclude=user,
|
||||||
content=InvenTreeNotificationBodies.ItemsReceived,
|
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 the associated barcode model type code for this model."""
|
||||||
return 'SO'
|
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):
|
def __str__(self):
|
||||||
"""Render a string representation of this SalesOrder."""
|
"""Render a string representation of this SalesOrder."""
|
||||||
return f'{self.reference} - {self.customer.name if self.customer else _("deleted")}'
|
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)
|
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
|
@property
|
||||||
def can_hold(self):
|
def can_hold(self):
|
||||||
"""Return True if this order can be placed on hold."""
|
"""Return True if this order can be placed on hold."""
|
||||||
@ -1325,6 +1367,7 @@ class SalesOrder(TotalPriceMixin, Order):
|
|||||||
SalesOrder,
|
SalesOrder,
|
||||||
exclude=self.created_by,
|
exclude=self.created_by,
|
||||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||||
|
extra_users=self.subscribed_users(),
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@ -1466,9 +1509,6 @@ def after_save_sales_order(sender, instance: SalesOrder, created: bool, **kwargs
|
|||||||
# Create default shipment
|
# Create default shipment
|
||||||
SalesOrderShipment.objects.create(order=instance, reference='1')
|
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):
|
class OrderLineItem(InvenTree.models.InvenTreeMetadataModel):
|
||||||
"""Abstract model for an order line item.
|
"""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 the associated barcode model type code for this model."""
|
||||||
return 'RO'
|
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):
|
def __str__(self):
|
||||||
"""Render a string representation of this ReturnOrder."""
|
"""Render a string representation of this ReturnOrder."""
|
||||||
return f'{self.reference} - {self.customer.name if self.customer else _("no customer")}'
|
return f'{self.reference} - {self.customer.name if self.customer else _("no customer")}'
|
||||||
@ -2439,6 +2494,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
ReturnOrder,
|
ReturnOrder,
|
||||||
exclude=self.created_by,
|
exclude=self.created_by,
|
||||||
content=InvenTreeNotificationBodies.OrderCanceled,
|
content=InvenTreeNotificationBodies.OrderCanceled,
|
||||||
|
extra_users=self.subscribed_users(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _action_complete(self, *args, **kwargs):
|
def _action_complete(self, *args, **kwargs):
|
||||||
@ -2471,6 +2527,15 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
|
|
||||||
trigger_event(ReturnOrderEvents.ISSUED, id=self.pk)
|
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
|
@transaction.atomic
|
||||||
def hold_order(self):
|
def hold_order(self):
|
||||||
"""Attempt to tranasition to ON_HOLD status."""
|
"""Attempt to tranasition to ON_HOLD status."""
|
||||||
@ -2575,6 +2640,7 @@ class ReturnOrder(TotalPriceMixin, Order):
|
|||||||
ReturnOrder,
|
ReturnOrder,
|
||||||
exclude=user,
|
exclude=user,
|
||||||
content=InvenTreeNotificationBodies.ReturnOrderItemsReceived,
|
content=InvenTreeNotificationBodies.ReturnOrderItemsReceived,
|
||||||
|
extra_users=line.item.part.get_subscribers(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
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 import transaction
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -14,15 +14,24 @@ import InvenTree.helpers_model
|
|||||||
import order.models
|
import order.models
|
||||||
from InvenTree.tasks import ScheduledTask, scheduled_task
|
from InvenTree.tasks import ScheduledTask, scheduled_task
|
||||||
from order.events import PurchaseOrderEvents, SalesOrderEvents
|
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 plugin.events import trigger_event
|
||||||
|
from users.models import Owner
|
||||||
|
|
||||||
logger = structlog.get_logger('inventree')
|
logger = structlog.get_logger('inventree')
|
||||||
|
|
||||||
|
|
||||||
def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
def notify_overdue_purchase_order(po: order.models.PurchaseOrder) -> None:
|
||||||
"""Notify users that a PurchaseOrder has just become 'overdue'."""
|
"""Notify users that a PurchaseOrder has just become 'overdue'.
|
||||||
targets = []
|
|
||||||
|
Arguments:
|
||||||
|
po: The PurchaseOrder object that is overdue.
|
||||||
|
"""
|
||||||
|
targets: list[User | Group | Owner] = []
|
||||||
|
|
||||||
if po.created_by:
|
if po.created_by:
|
||||||
targets.append(po.created_by)
|
targets.append(po.created_by)
|
||||||
@ -30,6 +39,8 @@ def notify_overdue_purchase_order(po: order.models.PurchaseOrder):
|
|||||||
if po.responsible:
|
if po.responsible:
|
||||||
targets.append(po.responsible)
|
targets.append(po.responsible)
|
||||||
|
|
||||||
|
targets.extend(po.subscribed_users())
|
||||||
|
|
||||||
name = _('Overdue Purchase Order')
|
name = _('Overdue Purchase Order')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
@ -86,9 +97,9 @@ def check_overdue_purchase_orders():
|
|||||||
notified_orders.add(line.order.pk)
|
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'."""
|
"""Notify appropriate users that a SalesOrder has just become 'overdue'."""
|
||||||
targets = []
|
targets: list[User, Group, Owner] = []
|
||||||
|
|
||||||
if so.created_by:
|
if so.created_by:
|
||||||
targets.append(so.created_by)
|
targets.append(so.created_by)
|
||||||
@ -96,6 +107,8 @@ def notify_overdue_sales_order(so: order.models.SalesOrder):
|
|||||||
if so.responsible:
|
if so.responsible:
|
||||||
targets.append(so.responsible)
|
targets.append(so.responsible)
|
||||||
|
|
||||||
|
targets.extend(so.subscribed_users())
|
||||||
|
|
||||||
name = _('Overdue Sales Order')
|
name = _('Overdue Sales Order')
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
@ -149,6 +162,71 @@ def check_overdue_sales_orders():
|
|||||||
notified_orders.add(line.order.pk)
|
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:
|
def complete_sales_order_shipment(shipment_id: int, user_id: int) -> None:
|
||||||
"""Complete allocations for a pending shipment against a SalesOrder.
|
"""Complete allocations for a pending shipment against a SalesOrder.
|
||||||
|
|
||||||
|
@ -333,18 +333,20 @@ class SalesOrderTest(TestCase):
|
|||||||
self.assertEqual(len(messages), 1)
|
self.assertEqual(len(messages), 1)
|
||||||
|
|
||||||
def test_new_so_notification(self):
|
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 responsible user should receive a notification
|
||||||
- The creating user should *not* receive a notification
|
- The creating user should *not* receive a notification
|
||||||
"""
|
"""
|
||||||
SalesOrder.objects.create(
|
so = SalesOrder.objects.create(
|
||||||
customer=self.customer,
|
customer=self.customer,
|
||||||
reference='1234567',
|
reference='1234567',
|
||||||
created_by=get_user_model().objects.get(pk=3),
|
created_by=get_user_model().objects.get(pk=3),
|
||||||
responsible=Owner.create(obj=Group.objects.get(pk=3)),
|
responsible=Owner.create(obj=Group.objects.get(pk=3)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
so.issue_order()
|
||||||
|
|
||||||
messages = NotificationMessage.objects.filter(category='order.new_salesorder')
|
messages = NotificationMessage.objects.filter(category='order.new_salesorder')
|
||||||
|
|
||||||
# A notification should have been generated for user 4 (who is a member of group 3)
|
# 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)
|
return prefetch.filter(category=self.id)
|
||||||
|
|
||||||
def get_subscribers(self, include_parents=True):
|
def get_subscribers(self, include_parents: bool = True) -> list[User]:
|
||||||
"""Return a list of users who subscribe to this PartCategory."""
|
"""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()
|
subscribers = set()
|
||||||
|
|
||||||
if include_parents:
|
if include_parents:
|
||||||
@ -1429,9 +1436,18 @@ class Part(
|
|||||||
"""
|
"""
|
||||||
return self.total_stock - self.allocation_count() + self.on_order
|
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.
|
"""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 user may 'subscribe' to this part in the following ways:
|
||||||
|
|
||||||
a) Subscribing to the part instance directly
|
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