Merge pull request #2246 from SchrodingersGat/build-order-notification

Build order notification
This commit is contained in:
Oliver 2021-11-04 15:23:39 +11:00 committed by GitHub
commit 170d8d11d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 16 deletions

View File

@ -9,16 +9,16 @@ import decimal
import os
from datetime import datetime
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.urls import reverse
from django.core.validators import MinValueValidator
from django.db import models, transaction
from django.db.models import Sum, Q
from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator
from django.db.models.signals import post_save
from django.dispatch.dispatcher import receiver
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from markdownx.models import MarkdownxField
@ -27,16 +27,17 @@ from mptt.exceptions import InvalidMove
from InvenTree.status_codes import BuildStatus, StockStatus, StockHistoryCode
from InvenTree.helpers import increment, getSetting, normalize, MakeBarcode
from InvenTree.validators import validate_build_order_reference
from InvenTree.models import InvenTreeAttachment, ReferenceIndexingMixin
from InvenTree.validators import validate_build_order_reference
import common.models
import InvenTree.fields
import InvenTree.helpers
import InvenTree.tasks
from stock import models as StockModels
from part import models as PartModels
from stock import models as StockModels
from users import models as UserModels
@ -1014,6 +1015,19 @@ class Build(MPTTModel, ReferenceIndexingMixin):
return self.status == BuildStatus.COMPLETE
@receiver(post_save, sender=Build, dispatch_uid='build_post_save_log')
def after_save_build(sender, instance: Build, created: bool, **kwargs):
"""
Callback function to be executed after a Build instance is saved
"""
if created:
# A new Build has just been created
# Run checks on required parts
InvenTree.tasks.offload_task('build.tasks.check_build_stock', instance)
class BuildOrderAttachment(InvenTreeAttachment):
"""
Model for storing file attachments against a BuildOrder object

96
InvenTree/build/tasks.py Normal file
View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal
import logging
from django.utils.translation import ugettext_lazy as _
from django.template.loader import render_to_string
from allauth.account.models import EmailAddress
import build.models
import InvenTree.helpers
import InvenTree.tasks
import part.models as part_models
logger = logging.getLogger('inventree')
def check_build_stock(build: build.models.Build):
"""
Check the required stock for a newly created build order,
and send an email out to any subscribed users if stock is low.
"""
# Iterate through each of the parts required for this build
lines = []
if not build:
logger.error("Invalid build passed to 'build.tasks.check_build_stock'")
return
try:
part = build.part
except part_models.Part.DoesNotExist:
# Note: This error may be thrown during unit testing...
logger.error("Invalid build.part passed to 'build.tasks.check_build_stock'")
return
for bom_item in part.get_bom_items():
sub_part = bom_item.sub_part
# The 'in stock' quantity depends on whether the bom_item allows variants
in_stock = sub_part.get_stock_count(include_variants=bom_item.allow_variants)
allocated = sub_part.allocation_count()
available = max(0, in_stock - allocated)
required = Decimal(bom_item.quantity) * Decimal(build.quantity)
if available < required:
# There is not sufficient stock for this part
lines.append({
'link': InvenTree.helpers.construct_absolute_url(sub_part.get_absolute_url()),
'part': sub_part,
'in_stock': in_stock,
'allocated': allocated,
'available': available,
'required': required,
})
if len(lines) == 0:
# Nothing to do
return
# Are there any users subscribed to these parts?
subscribers = build.part.get_subscribers()
emails = EmailAddress.objects.filter(
user__in=subscribers,
)
if len(emails) > 0:
logger.info(f"Notifying users of stock required for build {build.pk}")
context = {
'link': InvenTree.helpers.construct_absolute_url(build.get_absolute_url()),
'build': build,
'part': build.part,
'lines': lines,
}
# Render the HTML message
html_message = render_to_string('email/build_order_required_stock.html', context)
subject = "[InvenTree] " + _("Stock required for build order")
recipients = emails.values_list('email', flat=True)
InvenTree.tasks.send_email(subject, '', recipients, html_message=html_message)

View File

@ -1324,6 +1324,17 @@ class Part(MPTTModel):
return query
def get_stock_count(self, include_variants=True):
"""
Return the total "in stock" count for this part
"""
entries = self.stock_entries(in_stock=True, include_variants=include_variants)
query = entries.aggregate(t=Coalesce(Sum('quantity'), Decimal(0)))
return query['t']
@property
def total_stock(self):
""" Return the total stock quantity for this part.
@ -1332,11 +1343,7 @@ class Part(MPTTModel):
- If this part is a "template" (variants exist) then these are counted too
"""
entries = self.stock_entries(in_stock=True)
query = entries.aggregate(t=Coalesce(Sum('quantity'), Decimal(0)))
return query['t']
return self.get_stock_count()
def get_bom_item_filter(self, include_inherited=True):
"""
@ -2095,13 +2102,16 @@ class Part(MPTTModel):
@receiver(post_save, sender=Part, dispatch_uid='part_post_save_log')
def after_save_part(sender, instance: Part, **kwargs):
def after_save_part(sender, instance: Part, created, **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)
if not created:
# Check part stock only if we are *updating* the part (not creating it)
# Run this check in the background
InvenTree.tasks.offload_task('part.tasks.notify_low_stock_if_required', instance)
def attach_file(instance, filename):

View File

@ -50,7 +50,7 @@ def notify_low_stock(part: part.models.Part):
'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()),
}
subject = _(f'[InvenTree] {part.name} is low on stock')
subject = "[InvenTree] " + _("Low stock notification")
html_message = render_to_string('email/low_stock_notification.html', context)
recipients = emails.values_list('email', flat=True)

View File

@ -0,0 +1,39 @@
{% extends "email/email.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block title %}
{% trans "Stock is required for the following build order" %}<br>
{% blocktrans with build=build.reference part=part.full_name quantity=build.quantity %}Build order {{ build }} - building {{ quantity }} x {{ part }}{% endblocktrans %}
<br>
<p>{% trans "Click on the following link to view this build order" %}: <a href='{{ link }}'>{{ link }}</a></p>
{% endblock title %}
{% block body %}
<tr colspan='100%' style='height: 2rem; text-align: center;'>{% trans "The following parts are low on required stock" %}</tr>
<tr style="height: 3rem; border-bottom: 1px solid">
<th>{% trans "Part" %}</th>
<th>{% trans "Required Quantity" %}</th>
<th>{% trans "Available" %}</th>
</tr>
{% for line in lines %}
<tr style="height: 2.5rem; border-bottom: 1px solid">
<td style='padding-left: 1em;'>
<a href='{{ line.link }}'>{{ line.part.full_name }}</a>{% if part.description %} - <em>{{ part.description }}</em>{% endif %}
</td>
<td style="text-align: center;">
{% decimal line.required %} {% if line.part.units %}{{ line.part.units }}{% endif %}
</td>
<td style="text-align: center;">{% decimal line.available %} {% if line.part.units %}{{ line.part.units }}{% endif %}</td>
</tr>
{% endfor %}
{% endblock body %}
{% block footer_prefix %}
<p><em>{% blocktrans with part=part.name %}You are receiving this email because you are subscribed to notifications for this part {% endblocktrans %}.</em></p>
{% endblock footer_prefix %}