diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index aa17ef8603..801c75aa26 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -52,7 +52,7 @@ def schedule_task(taskname, **kwargs): pass -def offload_task(taskname, force_sync=False, *args, **kwargs): +def offload_task(taskname, *args, force_sync=False, **kwargs): """ Create an AsyncTask if workers are running. This is different to a 'scheduled' task, @@ -108,7 +108,7 @@ def offload_task(taskname, force_sync=False, *args, **kwargs): return # Workers are not running: run it as synchronous task - _func() + _func(*args, **kwargs) def heartbeat(): @@ -290,7 +290,7 @@ def update_exchange_rates(): Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete() -def send_email(subject, body, recipients, from_email=None): +def send_email(subject, body, recipients, from_email=None, html_message=None): """ Send an email with the specified subject and body, to the specified recipients list. @@ -306,4 +306,5 @@ def send_email(subject, body, recipients, from_email=None): from_email, recipients, fail_silently=False, + html_message=html_message ) diff --git a/InvenTree/InvenTree/test_api.py b/InvenTree/InvenTree/test_api.py index dfe94c034e..6ace21b576 100644 --- a/InvenTree/InvenTree/test_api.py +++ b/InvenTree/InvenTree/test_api.py @@ -102,9 +102,9 @@ class APITests(InvenTreeAPITestCase): fixtures = [ 'location', - 'stock', - 'part', 'category', + 'part', + 'stock' ] token = None diff --git a/InvenTree/part/models.py b/InvenTree/part/models.py index 5cd9fa3180..050b46058a 100644 --- a/InvenTree/part/models.py +++ b/InvenTree/part/models.py @@ -1988,6 +1988,9 @@ class Part(MPTTModel): def related_count(self): return len(self.get_related_parts()) + def is_part_low_on_stock(self): + return self.total_stock <= self.minimum_stock + 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 new file mode 100644 index 0000000000..9fc05ec3f1 --- /dev/null +++ b/InvenTree/part/tasks.py @@ -0,0 +1,52 @@ +# Author: Roche Christopher +# Created at 10:26 AM on 31/10/21 + +import logging + +from django.utils.translation import ugettext_lazy as _ +from django.template.loader import render_to_string + +from InvenTree import tasks as inventree_tasks +from part.models import Part + +logger = logging.getLogger("inventree") + + +def notify_low_stock(part: Part): + """ + Notify users who have starred a part when its stock quantity falls below the minimum threshold + """ + + from allauth.account.models import EmailAddress + starred_users_email = EmailAddress.objects.filter(user__starred_parts__part=part) + + if len(starred_users_email) > 0: + logger.info(f"Notify users regarding low stock of {part.name}") + context = { + 'part_name': part.name, + # Part url can be used to open the page of part in application from the email. + # It can be facilitated when the application base url is accessible programmatically. + # 'part_url': f'{application_base_url}/part/{stock_item.part.id}', + + # quantity is in decimal field datatype. Since the same datatype is used in models, + # it is not converted to number/integer, + 'part_quantity': part.total_stock, + 'minimum_quantity': part.minimum_stock + } + subject = _(f'Attention! {part.name} is low on stock') + html_message = render_to_string('stock/low_stock_notification.html', context) + recipients = starred_users_email.values_list('email', flat=True) + inventree_tasks.send_email(subject, '', recipients, html_message=html_message) + + +def notify_low_stock_if_required(part: Part): + """ + Check if the stock quantity has fallen below the minimum threshold of part. If yes, notify the users who have + starred the part + """ + + if part.is_part_low_on_stock(): + inventree_tasks.offload_task( + 'part.tasks.notify_low_stock', + part + ) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 1372e63406..657469a744 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -17,7 +17,7 @@ from django.db.models import Sum, Q 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, post_delete from django.dispatch import receiver from markdownx.models import MarkdownxField @@ -41,6 +41,7 @@ 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): @@ -1651,6 +1652,24 @@ def before_delete_stock_item(sender, instance, using, **kwargs): child.save() +@receiver(post_delete, sender=StockItem, dispatch_uid='stock_item_post_delete_log') +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) + + +@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 + """ + + part_tasks.notify_low_stock_if_required(instance.part) + + class StockItemAttachment(InvenTreeAttachment): """ Model for storing file attachments against a StockItem object. diff --git a/InvenTree/stock/templates/stock/low_stock_notification.html b/InvenTree/stock/templates/stock/low_stock_notification.html new file mode 100644 index 0000000000..3126cd11c1 --- /dev/null +++ b/InvenTree/stock/templates/stock/low_stock_notification.html @@ -0,0 +1,30 @@ +{% load i18n %} + +
{% trans "Hi, " %} {{ part_name }} {% trans "is low on stock. Kindly do the needful." %}
+ +{% trans "Part low on stock" %} | +||
---|---|---|
{% trans "Part Name" %} | +{% trans "Available Quantity" %} | +{% trans "Minimum Quantity" %} | +
{{ part_name }} | +{{ part_quantity }} | +{{ minimum_quantity }} | +
{% trans "You are receiving this mail because you have starred the part " %} {{ part_name }} + {% trans "Inventree application" %} + | +