mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #2208 from rocheparadox/master
Email notification for low stock
This commit is contained in:
commit
9cbc2b82b5
@ -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
|
||||||
)
|
)
|
||||||
|
@ -102,9 +102,9 @@ class APITests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
fixtures = [
|
fixtures = [
|
||||||
'location',
|
'location',
|
||||||
'stock',
|
|
||||||
'part',
|
|
||||||
'category',
|
'category',
|
||||||
|
'part',
|
||||||
|
'stock'
|
||||||
]
|
]
|
||||||
|
|
||||||
token = None
|
token = None
|
||||||
|
@ -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
52
InvenTree/part/tasks.py
Normal 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
|
||||||
|
)
|
@ -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.
|
||||||
|
30
InvenTree/stock/templates/stock/low_stock_notification.html
Normal file
30
InvenTree/stock/templates/stock/low_stock_notification.html
Normal 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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user