From d8796f95356730784b31ddd078c618780e3c3918 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Fri, 29 Oct 2021 16:03:41 +0530 Subject: [PATCH 1/8] Notify users who have starred a part when that part's stock quantity falls below the minimum quanitity/threshold through email. --- InvenTree/InvenTree/tasks.py | 36 +++++++++++++++++-- InvenTree/stock/models.py | 14 +++++++- .../stock/low_stock_notification.html | 27 ++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 InvenTree/stock/templates/stock/low_stock_notification.html diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index aa17ef8603..9987a2593d 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -11,6 +11,7 @@ from django.utils import timezone from django.core.exceptions import AppRegistryNotReady from django.db.utils import OperationalError, ProgrammingError +from django.template.loader import render_to_string logger = logging.getLogger("inventree") @@ -52,7 +53,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, @@ -290,7 +291,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 +307,35 @@ def send_email(subject, body, recipients, from_email=None): from_email, recipients, fail_silently=False, + html_message=html_message ) + + +def notify_low_stock(stock_item): + """ + Notify users who have starred a part when its stock quantity falls below the minimum threshold + """ + + from allauth.account.models import EmailAddress + starred_users = EmailAddress.objects.filter(user__starred_parts__part=stock_item.part) + + if len(starred_users) > 0: + logger.info(f"Notify users regarding low stock of {stock_item.part.name}") + body = f'Hi, {stock_item.part.name} is low on stock. Kindly do the needful.' + context = { + 'part_name': stock_item.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}', + + 'message': body, + + # quantity is in decimal field datatype. Since the same datatype is used in models, + # it is not converted to number/integer, + 'part_quantity': stock_item.quantity, + 'minimum_quantity': stock_item.part.minimum_stock + } + subject = f'Attention! {stock_item.part.name} is low on stock' + html_message = render_to_string('stock/low_stock_notification.html', context) + recipients = starred_users.values_list('email', flat=True) + send_email(subject, body, recipients, html_message=html_message) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index 1372e63406..ff8b91b105 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 from django.dispatch import receiver from markdownx.models import MarkdownxField @@ -36,6 +36,7 @@ import label.models from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField +from InvenTree import tasks as inventree_tasks from users.models import Owner @@ -1651,6 +1652,17 @@ def before_delete_stock_item(sender, instance, using, **kwargs): child.save() +@receiver(post_save, sender=StockItem) +def after_save_stock_item(sender, instance: StockItem, **kwargs): + """ + Check if the stock quantity has fallen below the minimum threshold of part. If yes, notify the users who have + starred the part + """ + + if instance.quantity <= instance.part.minimum_stock: + inventree_tasks.notify_low_stock(instance) + + 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..fa3799f6dd --- /dev/null +++ b/InvenTree/stock/templates/stock/low_stock_notification.html @@ -0,0 +1,27 @@ +

{{ message }}

+ + + + + + + + + + + + + + + + + + + + + + + +
Part low on stock
Part NameAvailable QuantityMinimum Quantity
{{ part_name }}{{ part_quantity }}{{ minimum_quantity }}
You are receiving this mail because you have starred the part {{ part_name }} in + Inventree application
+ From 83309fd054f0aa9cfab16e57a23a7eeecb4468e8 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Sat, 30 Oct 2021 08:16:42 +0530 Subject: [PATCH 2/8] Fixed the order of fixtures installation for testing --- InvenTree/InvenTree/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From e0cd02ee60a5ea42cd2735d4700a8ab34d9af950 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Sat, 30 Oct 2021 08:30:39 +0530 Subject: [PATCH 3/8] added dispatch_uid to post_save signal of StockItem --- InvenTree/stock/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index ff8b91b105..b4746e0879 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1652,7 +1652,7 @@ def before_delete_stock_item(sender, instance, using, **kwargs): child.save() -@receiver(post_save, sender=StockItem) +@receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log') def after_save_stock_item(sender, instance: StockItem, **kwargs): """ Check if the stock quantity has fallen below the minimum threshold of part. If yes, notify the users who have From 6ec2801fcea15d0784e0ad57f6000e8b568aa05b Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Sat, 30 Oct 2021 20:32:10 +0530 Subject: [PATCH 4/8] Facilitated translation for low stock notification subject moved the message/content of low stock notification to html template Facilitated translation in low stock notification html template file --- InvenTree/InvenTree/tasks.py | 8 +++----- .../stock/templates/stock/low_stock_notification.html | 9 ++++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 9987a2593d..da1d29d76a 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -12,6 +12,7 @@ from django.utils import timezone from django.core.exceptions import AppRegistryNotReady from django.db.utils import OperationalError, ProgrammingError from django.template.loader import render_to_string +from django.utils.translation import gettext_lazy as _ logger = logging.getLogger("inventree") @@ -321,21 +322,18 @@ def notify_low_stock(stock_item): if len(starred_users) > 0: logger.info(f"Notify users regarding low stock of {stock_item.part.name}") - body = f'Hi, {stock_item.part.name} is low on stock. Kindly do the needful.' context = { 'part_name': stock_item.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}', - 'message': body, - # quantity is in decimal field datatype. Since the same datatype is used in models, # it is not converted to number/integer, 'part_quantity': stock_item.quantity, 'minimum_quantity': stock_item.part.minimum_stock } - subject = f'Attention! {stock_item.part.name} is low on stock' + subject = _(f'Attention! {stock_item.part.name} is low on stock') html_message = render_to_string('stock/low_stock_notification.html', context) recipients = starred_users.values_list('email', flat=True) - send_email(subject, body, recipients, html_message=html_message) + send_email(subject, '', recipients, html_message=html_message) diff --git a/InvenTree/stock/templates/stock/low_stock_notification.html b/InvenTree/stock/templates/stock/low_stock_notification.html index fa3799f6dd..04ada64e18 100644 --- a/InvenTree/stock/templates/stock/low_stock_notification.html +++ b/InvenTree/stock/templates/stock/low_stock_notification.html @@ -1,4 +1,6 @@ -

{{ message }}

+{% load i18n %} + +

{% trans "Hi, " %} {{ part_name }} {% trans "is low on stock. Kindly do the needful." %}

@@ -19,8 +21,9 @@ - +
You are receiving this mail because you have starred the part {{ part_name }} in - Inventree application{% trans "You are receiving this mail because you have starred the part " %} {{ part_name }} + {% trans "Inventree application" %} +
From fca15a0439969181cfbafc3c93799f7e329df4ad Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Sun, 31 Oct 2021 11:21:06 +0530 Subject: [PATCH 5/8] added arbitrary args and arbitrary keyword args while executing a function synchronously from offload_task() in inventree.tasks --- InvenTree/InvenTree/tasks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index da1d29d76a..e623d7a98c 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -110,7 +110,7 @@ def offload_task(taskname, *args, force_sync=False, **kwargs): return # Workers are not running: run it as synchronous task - _func() + _func(*args, **kwargs) def heartbeat(): From 40da41959bffc677d940c1cd77245a7ddee3af9b Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Sun, 31 Oct 2021 11:26:41 +0530 Subject: [PATCH 6/8] Created part.tasks file and moved notify_low_stock function to the same from InvenTree.tasks. The argument type is changed from StockItem to Part Added trans to headers of table in email template of low_stock_notification.html added is_part_low_on_stock() function to the part model to check if the part's stock has fallen below the minimum quantity used offload_task function to run the low stock notification function asynchronously --- InvenTree/InvenTree/tasks.py | 27 ------------- InvenTree/part/models.py | 3 ++ InvenTree/part/tasks.py | 39 +++++++++++++++++++ InvenTree/stock/models.py | 7 +++- .../stock/low_stock_notification.html | 8 ++-- 5 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 InvenTree/part/tasks.py diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index e623d7a98c..4fa7409326 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -310,30 +310,3 @@ def send_email(subject, body, recipients, from_email=None, html_message=None): fail_silently=False, html_message=html_message ) - - -def notify_low_stock(stock_item): - """ - Notify users who have starred a part when its stock quantity falls below the minimum threshold - """ - - from allauth.account.models import EmailAddress - starred_users = EmailAddress.objects.filter(user__starred_parts__part=stock_item.part) - - if len(starred_users) > 0: - logger.info(f"Notify users regarding low stock of {stock_item.part.name}") - context = { - 'part_name': stock_item.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': stock_item.quantity, - 'minimum_quantity': stock_item.part.minimum_stock - } - subject = _(f'Attention! {stock_item.part.name} is low on stock') - html_message = render_to_string('stock/low_stock_notification.html', context) - recipients = starred_users.values_list('email', flat=True) - send_email(subject, '', recipients, html_message=html_message) 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..667e70f1a9 --- /dev/null +++ b/InvenTree/part/tasks.py @@ -0,0 +1,39 @@ +# 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) diff --git a/InvenTree/stock/models.py b/InvenTree/stock/models.py index b4746e0879..69b061d25a 100644 --- a/InvenTree/stock/models.py +++ b/InvenTree/stock/models.py @@ -1659,8 +1659,11 @@ def after_save_stock_item(sender, instance: StockItem, **kwargs): starred the part """ - if instance.quantity <= instance.part.minimum_stock: - inventree_tasks.notify_low_stock(instance) + if instance.part.is_part_low_on_stock(): + inventree_tasks.offload_task( + 'part.tasks.notify_low_stock', + instance.part + ) class StockItemAttachment(InvenTreeAttachment): diff --git a/InvenTree/stock/templates/stock/low_stock_notification.html b/InvenTree/stock/templates/stock/low_stock_notification.html index 04ada64e18..3126cd11c1 100644 --- a/InvenTree/stock/templates/stock/low_stock_notification.html +++ b/InvenTree/stock/templates/stock/low_stock_notification.html @@ -5,13 +5,13 @@ - + - - - + + + From 60c2aab06d395bac99e5dcd6ed6d034c55298d29 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Sun, 31 Oct 2021 11:30:14 +0530 Subject: [PATCH 7/8] remove unused imports --- InvenTree/InvenTree/tasks.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/InvenTree/InvenTree/tasks.py b/InvenTree/InvenTree/tasks.py index 4fa7409326..801c75aa26 100644 --- a/InvenTree/InvenTree/tasks.py +++ b/InvenTree/InvenTree/tasks.py @@ -11,8 +11,6 @@ from django.utils import timezone from django.core.exceptions import AppRegistryNotReady from django.db.utils import OperationalError, ProgrammingError -from django.template.loader import render_to_string -from django.utils.translation import gettext_lazy as _ logger = logging.getLogger("inventree") From 76c1e936db78424e0d6953c4062eb32863e302c6 Mon Sep 17 00:00:00 2001 From: rocheparadox Date: Mon, 1 Nov 2021 08:25:59 +0530 Subject: [PATCH 8/8] Added post_delete hook to StockItem moved the business logic of 'deciding if a low stock notification has to be sent' to part.tasks --- InvenTree/part/tasks.py | 13 +++++++++++++ InvenTree/stock/models.py | 22 +++++++++++++--------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index 667e70f1a9..9fc05ec3f1 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -37,3 +37,16 @@ def notify_low_stock(part: Part): 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 69b061d25a..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, post_save +from django.db.models.signals import pre_delete, post_save, post_delete from django.dispatch import receiver from markdownx.models import MarkdownxField @@ -36,12 +36,12 @@ import label.models from InvenTree.status_codes import StockStatus, StockHistoryCode from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.fields import InvenTreeModelMoneyField, InvenTreeURLField -from InvenTree import tasks as inventree_tasks 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): @@ -1652,18 +1652,22 @@ 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): """ - Check if the stock quantity has fallen below the minimum threshold of part. If yes, notify the users who have - starred the part + Hook function to be executed after StockItem object is saved/updated """ - if instance.part.is_part_low_on_stock(): - inventree_tasks.offload_task( - 'part.tasks.notify_low_stock', - instance.part - ) + part_tasks.notify_low_stock_if_required(instance.part) class StockItemAttachment(InvenTreeAttachment):
Part low on stock{% trans "Part low on stock" %}
Part NameAvailable QuantityMinimum Quantity{% trans "Part Name" %}{% trans "Available Quantity" %}{% trans "Minimum Quantity" %}