Improve notification of 'low stock' parts:

- Traverse up the variant tree
- Enable subscription by "category"
This commit is contained in:
Oliver 2021-11-04 00:28:10 +11:00
parent 1c6eb41341
commit 476a1342c1
4 changed files with 48 additions and 17 deletions

View File

@ -20,7 +20,7 @@ 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
from django.dispatch import receiver from django.dispatch import receiver
from jinja2 import Template from jinja2 import Template
@ -47,6 +47,7 @@ from InvenTree import validators
from InvenTree.models import InvenTreeTree, InvenTreeAttachment from InvenTree.models import InvenTreeTree, InvenTreeAttachment
from InvenTree.fields import InvenTreeURLField from InvenTree.fields import InvenTreeURLField
from InvenTree.helpers import decimal2string, normalize, decimal2money from InvenTree.helpers import decimal2string, normalize, decimal2money
import InvenTree.tasks
from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus from InvenTree.status_codes import BuildStatus, PurchaseOrderStatus, SalesOrderStatus
@ -56,6 +57,7 @@ from company.models import SupplierPart
from stock import models as StockModels from stock import models as StockModels
import common.models import common.models
import part.settings as part_settings import part.settings as part_settings
@ -2085,9 +2087,24 @@ class Part(MPTTModel):
return len(self.get_related_parts()) return len(self.get_related_parts())
def is_part_low_on_stock(self): 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 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): def attach_file(instance, filename):
""" Function for storing a file for a PartAttachment """ Function for storing a file for a PartAttachment

View File

@ -13,23 +13,28 @@ from common.models import InvenTree
import InvenTree.helpers import InvenTree.helpers
import InvenTree.tasks import InvenTree.tasks
from part.models import Part import part.models
logger = logging.getLogger("inventree") 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 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}") 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 # 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}") logger.info(f"Notify users regarding low stock of {part.name}")
context = { context = {
# Pass the "Part" object through to the template 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') subject = _(f'[InvenTree] {part.name} is low on stock')
html_message = render_to_string('email/low_stock_notification.html', context) 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) 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. 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 true, notify the users who have subscribed to the part
""" """
if part.is_part_low_on_stock(): # Run "up" the tree, to allow notification for "parent" parts
InvenTree.tasks.offload_task( parts = part.get_ancestors(include_self=True, ascending=True)
'part.tasks.notify_low_stock',
part for p in parts:
) if p.is_part_low_on_stock():
InvenTree.tasks.offload_task(
'part.tasks.notify_low_stock',
p
)

View File

@ -27,7 +27,9 @@ from mptt.managers import TreeManager
from decimal import Decimal, InvalidOperation from decimal import Decimal, InvalidOperation
from datetime import datetime, timedelta from datetime import datetime, timedelta
from InvenTree import helpers from InvenTree import helpers
import InvenTree.tasks
import common.models import common.models
import report.models import report.models
@ -41,7 +43,6 @@ 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):
@ -1658,16 +1659,18 @@ def after_delete_stock_item(sender, instance: StockItem, **kwargs):
Function to be executed after a StockItem object is deleted 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') @receiver(post_save, sender=StockItem, dispatch_uid='stock_item_post_save_log')
def after_save_stock_item(sender, instance: StockItem, **kwargs): 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): class StockItemAttachment(InvenTreeAttachment):

View File

@ -17,13 +17,15 @@
{% block body %} {% block body %}
<tr style="height: 3rem; border-bottom: 1px solid"> <tr style="height: 3rem; border-bottom: 1px solid">
<th>{% trans "Part Name" %}</th> <th>{% trans "Part Name" %}</th>
<th>{% trans "Available Quantity" %}</th> <th>{% trans "Total Stock" %}</th>
<th>{% trans "Available" %}</th>
<th>{% trans "Minimum Quantity" %}</th> <th>{% trans "Minimum Quantity" %}</th>
</tr> </tr>
<tr style="height: 3rem"> <tr style="height: 3rem">
<td style="text-align: center;">{{ part.full_name }}</td> <td style="text-align: center;">{{ part.full_name }}</td>
<td style="text-align: center;">{{ part.total_stock }}</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> <td style="text-align: center;">{{ part.minimum_stock }}</td>
</tr> </tr>
{% endblock %} {% endblock %}