diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index dada6f125b..a3c294ea17 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -20,7 +20,7 @@ from django.db.models.functions import Coalesce from django.core.validators import MinValueValidator from django.contrib.auth.models import User -from django.db.models.signals import pre_delete +from django.db.models.signals import pre_delete, post_save from django.dispatch import receiver from jinja2 import Template @@ -47,6 +47,7 @@ from InvenTree import validators from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeURLField from InvenTree.helpers import decimal2string, normalize, decimal2money +import InvenTree.tasks from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus @@ -56,6 +57,7 @@ from company.models import SupplierPart from stock import models as StockModels import common.models + import part.settings as part_settings @@ -2085,9 +2087,24 @@ class Part(MPTTModel): return len(self.get_related_parts()) def is_part_low_on_stock(self): + """ + Returns True if the total stock for this part is less than the minimum stock level + """ + return self.total_stock <= self.minimum_stock + +@receiver(post_save, sender=Part, dispatch_uid='part_post_save_log') +def after_save_part(sender, instance: Part, **kwargs): + """ + Function to be executed after a Part is saved + """ + + # Run this check in the background + InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance) + + def attach_file(instance, filename): """ Function for storing a file for a PartAttachment diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index 72d996e772..779027a96d 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -13,23 +13,28 @@ from common.models import InvenTree import InvenTree.helpers import InvenTree.tasks -from part.models import Part +import part.models logger = logging.getLogger("inventree") -def notify_low_stock(part: Part): +def notify_low_stock(part: part.models.Part): """ Notify users who have starred a part when its stock quantity falls below the minimum threshold """ logger.info(f"Sending low stock notification email for {part.full_name}") - starred_users_email = EmailAddress.objects.filter(user__starred_parts__part=part) + # Get a list of users who are subcribed to this part + subscribers = part.get_subscribers() + + emails = EmailAddress.objects.filter( + user__in=subscribers, + ) # TODO: In the future, include the part image in the email template - if len(starred_users_email) > 0: + if len(emails) > 0: logger.info(f"Notify users regarding low stock of {part.name}") context = { # Pass the "Part" object through to the template context @@ -39,20 +44,24 @@ def notify_low_stock(part: Part): subject = _(f'[InvenTree] {part.name} is low on stock') html_message = render_to_string('email/low_stock_notification.html', context) - recipients = starred_users_email.values_list('email', flat=True) + recipients = emails.values_list('email', flat=True) InvenTree.tasks.send_email(subject, '', recipients, html_message=html_message) -def notify_low_stock_if_required(part: Part): +def notify_low_stock_if_required(part: part.models.Part): """ Check if the stock quantity has fallen below the minimum threshold of part. If true, notify the users who have subscribed to the part """ - if part.is_part_low_on_stock(): - InvenTree.tasks.offload_task( - 'part.tasks.notify_low_stock', - part - ) + # Run "up" the tree, to allow notification for "parent" parts + parts = part.get_ancestors(include_self=True, ascending=True) + + for p in parts: + if p.is_part_low_on_stock(): + InvenTree.tasks.offload_task( + 'part.tasks.notify_low_stock', + p + ) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 657469a744..eb0e6aa12f 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -27,7 +27,9 @@ from mptt.managers import TreeManager from decimal import Decimal, InvalidOperation from datetime import datetime, timedelta + from InvenTree import helpers +import InvenTree.tasks import common.models import report.models @@ -41,7 +43,6 @@ from users.models import Owner from company import models as CompanyModels from part import models as PartModels -from part import tasks as part_tasks class StockLocation(InvenTreeTree): @@ -1658,16 +1659,18 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs): Function to be executed after a StockItem object is deleted """ - part_tasks.notify_low_stock_if_required(instance.part) + # Run this check in the background + InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part) @receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log') def after_save_stock_item(sender, instance: StockItem, **kwargs): """ - Hook function to be executed after StockItem object is saved/updated + Hook function to be executed after StockItem object is saved/updated """ - part_tasks.notify_low_stock_if_required(instance.part) + # Run this check in the background + InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance.part) class StockItemAttachment(InvenTreeAttachment): diff --git a/InvenTree/templates/email/low_stock_notification.html b/InvenTree/templates/email/low_stock_notification.html index ecb350925a..4db9c2ddaa 100644 --- a/InvenTree/templates/email/low_stock_notification.html +++ b/InvenTree/templates/email/low_stock_notification.html @@ -17,13 +17,15 @@ {% block body %} <tr style="height: 3rem; border-bottom: 1px solid"> <th>{% trans "Part Name" %}</th> - <th>{% trans "Available Quantity" %}</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.total_stock }}</td> + <td style="text-align: center;">{{ part.available_stock }}</td> <td style="text-align: center;">{{ part.minimum_stock }}</td> </tr> {% endblock %}