Merge pull request #2208 from rocheparadox/master

Email notification for low stock
This commit is contained in:
Oliver 2021-11-01 22:45:49 +11:00 committed by GitHub
commit 9cbc2b82b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 111 additions and 6 deletions

View File

@ -52,7 +52,7 @@ def schedule_task(taskname, **kwargs):
pass 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. Create an AsyncTask if workers are running.
This is different to a 'scheduled' task, This is different to a 'scheduled' task,
@ -108,7 +108,7 @@ def offload_task(taskname, force_sync=False, *args, **kwargs):
return return
# Workers are not running: run it as synchronous task # Workers are not running: run it as synchronous task
_func() _func(*args, **kwargs)
def heartbeat(): def heartbeat():
@ -290,7 +290,7 @@ def update_exchange_rates():
Rate.objects.filter(backend="InvenTreeExchange").exclude(currency__in=currency_codes()).delete() 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, Send an email with the specified subject and body,
to the specified recipients list. to the specified recipients list.
@ -306,4 +306,5 @@ def send_email(subject, body, recipients, from_email=None):
from_email, from_email,
recipients, recipients,
fail_silently=False, fail_silently=False,
html_message=html_message
) )

View File

@ -102,9 +102,9 @@ class APITests(InvenTreeAPITestCase):
fixtures = [ fixtures = [
'location', 'location',
'stock',
'part',
'category', 'category',
'part',
'stock'
] ]
token = None token = None

View File

@ -1988,6 +1988,9 @@ class Part(MPTTModel):
def related_count(self): def related_count(self):
return len(self.get_related_parts()) 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): def attach_file(instance, filename):
""" Function for storing a file for a PartAttachment """ Function for storing a file for a PartAttachment

52
InvenTree/part/tasks.py Normal file
View File

@ -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
)

View File

@ -17,7 +17,7 @@ from django.db.models import Sum, Q
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.contrib.auth.models import User 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 django.dispatch import receiver
from markdownx.models import MarkdownxField from markdownx.models import MarkdownxField
@ -41,6 +41,7 @@ from users.models import Owner
from company import models as CompanyModels from company import models as CompanyModels
from part import models as PartModels from part import models as PartModels
from part import tasks as part_tasks
class StockLocation(InvenTreeTree): class StockLocation(InvenTreeTree):
@ -1651,6 +1652,24 @@ def before_delete_stock_item(sender, instance, using, **kwargs):
child.save() 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): class StockItemAttachment(InvenTreeAttachment):
""" """
Model for storing file attachments against a StockItem object. Model for storing file attachments against a StockItem object.

View File

@ -0,0 +1,30 @@
{% load i18n %}
<p>{% trans "Hi, " %} {{ part_name }} {% trans "is low on stock. Kindly do the needful." %}</p>
<table style="border-collapse:collapse; width: 80%;margin-left: 10%; font-size: 1rem">
<tr style="background: aliceblue; height: 4rem;">
<th colspan="3" style="padding-bottom: 1rem; font-size: 1.5rem; color:rgb(210,0, 0)">{% trans "Part low on stock" %}</th>
</tr>
<tr style="height: 3rem; border-bottom: 1px solid">
<th>{% trans "Part Name" %}</th>
<th>{% trans "Available Quantity" %}</th>
<th>{% trans "Minimum Quantity" %}</th>
</tr>
<tr style="height: 3rem">
<td style="text-align: center">{{ part_name }}</td>
<td style="text-align: center">{{ part_quantity }}</td>
<td style="text-align: center">{{ minimum_quantity }}</td>
</tr>
<tr style="background-color: aliceblue;height: 4rem;">
<td colspan="3" style="padding-top:1rem; text-align: center">{% trans "You are receiving this mail because you have starred the part " %} {{ part_name }}
{% trans "Inventree application" %}
</td>
</tr>
</table>