2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-04-29 12:06:44 +00:00

Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue2201

This commit is contained in:
Matthias 2021-11-03 00:25:39 +01:00
commit e3b02e596e
No known key found for this signature in database
GPG Key ID: F50EF5741D33E076
18 changed files with 314 additions and 113 deletions

View File

@ -69,6 +69,35 @@ def getStaticUrl(filename):
return os.path.join(STATIC_URL, str(filename)) return os.path.join(STATIC_URL, str(filename))
def construct_absolute_url(*arg):
"""
Construct (or attempt to construct) an absolute URL from a relative URL.
This is useful when (for example) sending an email to a user with a link
to something in the InvenTree web framework.
This requires the BASE_URL configuration option to be set!
"""
base = str(InvenTreeSetting.get_setting('INVENTREE_BASE_URL'))
url = '/'.join(arg)
if not base:
return url
# Strip trailing slash from base url
if base.endswith('/'):
base = base[:-1]
if url.startswith('/'):
url = url[1:]
url = f"{base}/{url}"
return url
def getBlankImage(): def getBlankImage():
""" """
Return the qualified path for the 'blank image' placeholder. Return the qualified path for the 'blank image' placeholder.

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

@ -807,19 +807,19 @@ class InvenTreeSetting(BaseInvenTreeSetting):
# login / SSO # login / SSO
'LOGIN_ENABLE_PWD_FORGOT': { 'LOGIN_ENABLE_PWD_FORGOT': {
'name': _('Enable password forgot'), 'name': _('Enable password forgot'),
'description': _('Enable password forgot function on the login-pages'), 'description': _('Enable password forgot function on the login pages'),
'default': True, 'default': True,
'validator': bool, 'validator': bool,
}, },
'LOGIN_ENABLE_REG': { 'LOGIN_ENABLE_REG': {
'name': _('Enable registration'), 'name': _('Enable registration'),
'description': _('Enable self-registration for users on the login-pages'), 'description': _('Enable self-registration for users on the login pages'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },
'LOGIN_ENABLE_SSO': { 'LOGIN_ENABLE_SSO': {
'name': _('Enable SSO'), 'name': _('Enable SSO'),
'description': _('Enable SSO on the login-pages'), 'description': _('Enable SSO on the login pages'),
'default': False, 'default': False,
'validator': bool, 'validator': bool,
}, },

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

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

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
from django.utils.translation import ugettext_lazy as _
from django.template.loader import render_to_string
from allauth.account.models import EmailAddress
from common.models import InvenTree
import InvenTree.helpers
import 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
"""
logger.info(f"Sending low stock notification email for {part.full_name}")
starred_users_email = EmailAddress.objects.filter(user__starred_parts__part=part)
# TODO: In the future, include the part image in the email template
if len(starred_users_email) > 0:
logger.info(f"Notify users regarding low stock of {part.name}")
context = {
# Pass the "Part" object through to the template context
'part': part,
'link': InvenTree.helpers.construct_absolute_url(part.get_absolute_url()),
}
subject = _(f'[InvenTree] {part.name} is low on stock')
html_message = render_to_string('email/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 true, notify the users who have subscribed to the part
"""
if part.is_part_low_on_stock():
InvenTree.tasks.offload_task(
'part.tasks.notify_low_stock',
part
)

View File

@ -122,6 +122,12 @@ def inventree_title(*args, **kwargs):
return version.inventreeInstanceTitle() return version.inventreeInstanceTitle()
@register.simple_tag()
def inventree_base_url(*args, **kwargs):
""" Return the INVENTREE_BASE_URL setting """
return InvenTreeSetting.get_setting('INVENTREE_BASE_URL')
@register.simple_tag() @register.simple_tag()
def python_version(*args, **kwargs): def python_version(*args, **kwargs):
""" """

View File

@ -14,6 +14,8 @@ from stock.models import StockItem
from common.models import InvenTreeSetting from common.models import InvenTreeSetting
import InvenTree.helpers
register = template.Library() register = template.Library()
@ -119,18 +121,10 @@ def internal_link(link, text):
text = str(text) text = str(text)
base_url = InvenTreeSetting.get_setting('INVENTREE_BASE_URL') url = InvenTree.helpers.construct_absolute_url(link)
# If the base URL is not set, just return the text # If the base URL is not set, just return the text
if not base_url: if not url:
return text return text
if not base_url.endswith('/'):
base_url += '/'
if base_url.endswith('/') and link.startswith('/'):
link = link[1:]
url = f"{base_url}{link}"
return mark_safe(f'<a href="{url}">{text}</a>') return mark_safe(f'<a href="{url}">{text}</a>')

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

@ -7,6 +7,12 @@
{% trans "Category Settings" %} {% trans "Category Settings" %}
{% endblock %} {% endblock %}
{% block actions %}
<button class='btn btn-success' id='new-cat-param' disabled=''>
<div class='fas fa-plus-circle'></div> {% trans "New Parameter" %}
</button>
{% endblock %}
{% block content %} {% block content %}
<div class='row'> <div class='row'>
@ -21,12 +27,6 @@
</form> </form>
</div> </div>
<div id='cat-param-buttons'>
<button class='btn btn-success' id='new-cat-param' disabled=''>
<div class='fas fa-plus-circle'></div> {% trans "New Parameter" %}
</button>
</div>
<table class='table table-striped table-condensed' id='cat-param-table' data-toolbar='#cat-param-buttons'> <table class='table table-striped table-condensed' id='cat-param-table' data-toolbar='#cat-param-buttons'>
</table> </table>

View File

@ -13,29 +13,31 @@
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tbody> <tbody>
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %} {% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %}
</tbody>
</table>
<table class='table table-striped table-condensed'>
<tbody>
<tr> <tr>
<td></td>
<th>{% trans "Base Currency" %}</th> <th>{% trans "Base Currency" %}</th>
<th>{{ base_currency }}</th> <th>{{ base_currency }}</th>
</tr> </tr>
<tr> <tr>
<th colspan='2'>{% trans "Exchange Rates" %}</th> <td></td>
<th colspan='4'>{% trans "Exchange Rates" %}</th>
</tr> </tr>
{% for rate in rates %} {% for rate in rates %}
<tr> <tr>
<td>{{ rate.currency }}</td> <td></td>
<td>{{ rate.value }}</td> <td>{{ rate.value }}</td>
<td>{{ rate.currency }}</td>
<td></td>
<td></td>
</tr> </tr>
{% endfor %} {% endfor %}
<tr> <tr>
<th></th>
<th> <th>
{% trans "Last Update" %} {% trans "Last Update" %}
</th> </th>
<td> <td colspan="3">
{% if rates_updated %} {% if rates_updated %}
{{ rates_updated }} {{ rates_updated }}
{% else %} {% else %}
@ -44,7 +46,7 @@
<form action='{% url "settings-currencies-refresh" %}' method='post'> <form action='{% url "settings-currencies-refresh" %}' method='post'>
<div id='refresh-rates-form'> <div id='refresh-rates-form'>
{% csrf_token %} {% csrf_token %}
<button type='submit' id='update-rates' class='btn btn-outline-secondary float-right'>{% trans "Update Now" %}</button> <button type='submit' id='update-rates' class='btn btn-primary float-right'>{% trans "Update Now" %}</button>
</div> </div>
</form> </form>
</td> </td>

View File

@ -18,7 +18,7 @@
{% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-info-circle" %} {% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-info-circle" %}
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENFORCE_MFA" %} {% include "InvenTree/settings/setting.html" with key="LOGIN_ENFORCE_MFA" %}
<tr> <tr>
<td>{% trans 'Signup' %}</td> <th><h5>{% trans 'Signup' %}</h5></th>
<td colspan='4'></td> <td colspan='4'></td>
</tr> </tr>
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-info-circle" %} {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-info-circle" %}

View File

@ -9,8 +9,6 @@
{% block content %} {% block content %}
<h4>{% trans "Part Options" %}</h4>
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tbody> <tbody>
{% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %} {% include "InvenTree/settings/setting.html" with key="PART_IPN_REGEX" %}
@ -40,12 +38,17 @@
</tbody> </tbody>
</table> </table>
<h4>{% trans "Part Import" %}</h4> <div class='panel-heading'>
<div class='d-flex flex-span'>
<button class='btn btn-success' id='import-part'> <h4>{% trans "Part Import" %}</h4>
<span class='fas fa-plus-circle'></span> {% trans "Import Part" %} {% include "spacer.html" %}
</button> <div class='btn-group' role='group'>
<button class='btn btn-success' id='import-part'>
<span class='fas fa-plus-circle'></span> {% trans "Import Part" %}
</button>
</div>
</div>
</div>
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tbody> <tbody>
@ -53,14 +56,16 @@
</tbody> </tbody>
</table> </table>
<div class='panel-heading'>
<span class='d-flex flex-span'>
<h4>{% trans "Part Parameter Templates" %}</h4> <h4>{% trans "Part Parameter Templates" %}</h4>
{% include "spacer.html" %}
<div id='param-buttons'> <div class='btn-group' role='group'>
<button class='btn btn-success' id='new-param'> <button class='btn btn-success' id='new-param'>
<span class='fas fa-plus-circle'></span> {% trans "New Parameter" %} <span class='fas fa-plus-circle'></span> {% trans "New Parameter" %}
</button> </button>
</div>
</span>
</div> </div>
<table class='table table-striped table-condensed' id='param-table' data-toolbar='#param-buttons'> <table class='table table-striped table-condensed' id='param-table' data-toolbar='#param-buttons'>

View File

@ -21,15 +21,13 @@
</div> </div>
{% else %} {% else %}
<div id='setting-{{ setting.pk }}'> <div id='setting-{{ setting.pk }}'>
<strong>
<span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'> <span id='setting-value-{{ setting.key.upper }}' fieldname='{{ setting.key.upper }}'>
{% if setting.value %} {% if setting.value %}
{{ setting.value }} <strong>{{ setting.value }}</strong>
{% else %} {% else %}
<em>{% trans "No value set" %}</em> <em style='color: #855;'>{% trans "No value set" %}</em>
{% endif %} {% endif %}
</span> </span>
</strong>
{{ setting.units }} {{ setting.units }}
</div> </div>
{% endif %} {% endif %}

View File

@ -11,18 +11,18 @@
{% trans "Account Settings" %} {% trans "Account Settings" %}
{% endblock %} {% endblock %}
{% block actions %}
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
</div>
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
<span class='fas fa-key'></span> {% trans "Set Password" %}
</div>
{% endblock %}
{% block content %} {% block content %}
{% mail_configured as mail_conf %} {% mail_configured as mail_conf %}
<div class='btn-group' style='float: right;'>
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
</div>
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
<span class='fas fa-key'></span> {% trans "Set Password" %}
</div>
</div>
<table class='table table-striped table-condensed'> <table class='table table-striped table-condensed'>
<tr> <tr>
<td>{% trans "Username" %}</td> <td>{% trans "Username" %}</td>
@ -40,10 +40,12 @@
<div class="row"> <div class="row">
<div class='panel-heading'> <div class='panel-heading'>
<div class='d-flex flex-span'>
<h4>{% trans "Email" %}</h4> <h4>{% trans "Email" %}</h4>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-sm-6">
{% if user.emailaddress_set.all %} {% if user.emailaddress_set.all %}
<p>{% trans 'The following email addresses are associated with your account:' %}</p> <p>{% trans 'The following email addresses are associated with your account:' %}</p>
@ -52,20 +54,26 @@
<fieldset class="blockLabels"> <fieldset class="blockLabels">
{% for emailaddress in user.emailaddress_set.all %} {% for emailaddress in user.emailaddress_set.all %}
<div>
<div class="ctrlHolder"> <div class="ctrlHolder">
<label for="email_radio_{{forloop.counter}}" <label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}">
class="{% if emailaddress.primary %}primary_email{%endif%}">
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked" {%endif %} value="{{emailaddress.email}}" /> <input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked" {%endif %} value="{{emailaddress.email}}" />
{{ emailaddress.email }} {% if emailaddress.primary %}
{% if emailaddress.verified %} <b>{{ emailaddress.email }}</b>
<span class="verified">{% trans "Verified" %}</span>
{% else %} {% else %}
<span class="unverified">{% trans "Unverified" %}</span> {{ emailaddress.email }}
{% endif %} {% endif %}
{% if emailaddress.primary %}<span class="primary">{% trans "Primary" %}</span>{% endif %}
</label> </label>
{% if emailaddress.verified %}
<span class='badge badge-right rounded-pill bg-success'>{% trans "Verified" %}</span>
{% else %}
<span class='badge badge-right rounded-pill bg-warning'>{% trans "Unverified" %}</span>
{% endif %}
{% if emailaddress.primary %}<span class='badge badge-right rounded-pill bg-primary'>{% trans "Primary" %}</span>{% endif %}
</div>
</div> </div>
{% endfor %} {% endfor %}
@ -88,7 +96,7 @@
</div> </div>
{% if can_add_email %} {% if can_add_email %}
<div class="col-md-6"> <div class="col-sm-6">
<h5>{% trans "Add Email Address" %}</h5> <h5>{% trans "Add Email Address" %}</h5>
<form method="post" action="{% url 'account_email' %}" class="add_email"> <form method="post" action="{% url 'account_email' %}" class="add_email">
@ -207,26 +215,26 @@
<h4>{% trans "Theme Settings" %}</h4> <h4>{% trans "Theme Settings" %}</h4>
</div> </div>
<form action='{% url "settings-appearance" %}' method='post'> <div class='col-sm-6'>
{% csrf_token %} <form action='{% url "settings-appearance" %}' method='post'>
<input name='next' type='hidden' value='{% url "settings" %}'> {% csrf_token %}
<div class="col-sm-6" style="width: 200px;"> <input name='next' type='hidden' value='{% url "settings" %}'>
<div id="div_id_themes" class="form-group"> <label for='theme' class=' requiredField'>
<div class="controls "> {% trans "Select theme" %}
<select name='theme' class='select form-control'> </label>
{% get_available_themes as themes %} <div class='form-group input-group mb-3'>
{% for theme in themes %} <select id='theme' name='theme' class='select form-control'>
<option value='{{ theme.key }}'>{{ theme.name }}</option> {% get_available_themes as themes %}
{% endfor %} {% for theme in themes %}
</select> <option value='{{ theme.key }}'>{{ theme.name }}</option>
{% endfor %}
</select>
<div class='input-group-append'>
<input type="submit" value="{% trans 'Set Theme' %}" class="btn btn-primary">
</div> </div>
</div> </div>
</div> </form>
<div class="col-sm-6" style="width: auto;"> </div>
<input type="submit" value="{% trans 'Set Theme' %}" class="btn btn btn-primary">
</div>
</form>
</div> </div>
<div class="row"> <div class="row">
@ -238,33 +246,32 @@
<form action="{% url 'set_language' %}" method="post"> <form action="{% url 'set_language' %}" method="post">
{% csrf_token %} {% csrf_token %}
<input name="next" type="hidden" value="{% url 'settings' %}"> <input name="next" type="hidden" value="{% url 'settings' %}">
<div class="col-sm-6" style="width: 200px;"> <label for='language' class=' requiredField'>
<div id="div_id_language" class="form-group"> {% trans "Select language" %}
<div class="controls "> </label>
<select name="language" class="select form-control"> <div class='form-group input-group mb-3'>
{% get_current_language as LANGUAGE_CODE %} <select name="language" class="select form-control">
{% get_available_languages as LANGUAGES %} {% get_current_language as LANGUAGE_CODE %}
{% get_language_info_list for LANGUAGES as languages %} {% get_available_languages as LANGUAGES %}
{% for language in languages %} {% get_language_info_list for LANGUAGES as languages %}
{% define language.code as lang_code %} {% for language in languages %}
{% define locale_stats|keyvalue:lang_code as lang_translated %} {% define language.code as lang_code %}
<option value="{{ lang_code }}" {% if lang_code == LANGUAGE_CODE %} selected{% endif %}> {% define locale_stats|keyvalue:lang_code as lang_translated %}
{{ language.name_local }} ({{ lang_code }}) <option value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
{% if lang_translated %} {{ language.name_local }} ({{ lang_code }})
{% if lang_translated %}
{% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %} {% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %}
{% else %} {% else %}
{% trans 'No translations available' %} {% trans 'No translations available' %}
{% endif %} {% endif %}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div> <div class='input-group-append'>
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
</div> </div>
</div> </div>
<div class="col-sm-6" style="width: auto;"> </form>
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
</div>
</form>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<h4>{% trans "Help the translation efforts!" %}</h4> <h4>{% trans "Help the translation efforts!" %}</h4>

View File

@ -0,0 +1,43 @@
{% load i18n %}
{% load static %}
{% load inventree_extras %}
<table style='border-collapse: collapse; width: 85%; margin-left: 10%; font-size: 1rem; border: 1px solid #68686a; border-radius: 2px;'>
{% block header %}
<tr style='background: #eef3f7; height: 4rem; text-align: center;'>
<th colspan="100%" style="padding-bottom: 1rem; color: #68686a;">
{% block header_row %}
<p style='font-size: 1.25rem;'>{% block title %}<!-- email title goes here -->{% endblock %}</p>
{% block subtitle %}
<!-- email subtitle goes here -->
{% endblock %}
{% endblock %}
</th>
</tr>
{% endblock %}
{% block body %}
<tr style="height: 3rem; border-bottom: 1px solid #68686a;">
{% block body_row %}
<!-- email body goes here -->
{% endblock %}
</tr>
{% endblock %}
{% block footer %}
<tr style='background: #eef3f7; height: 2rem;'>
<td colspan="100%" style="padding-top:1rem; text-align: center">
{% block footer_prefix %}
<!-- Custom footer information goes here -->
{% endblock %}
<p><em><small>{% trans "InvenTree version" %}: {% inventree_version %} - <a href='https://inventree.readthedocs.io'>inventree.readthedocs.io</a></small></em></p>
{% block footer_suffix %}
<!-- Custom footer information goes here -->
{% endblock %}
</td>
</tr>
{% endblock %}
</table>

View File

@ -0,0 +1,29 @@
{% extends "email/email.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block title %}
{% blocktrans with part=part.name %} The available stock for {{ part }} has fallen below the configured minimum level{% endblocktrans %}
{% if link %}
<p>{% trans "Click on the following link to view this part" %}: <a href="{{ link }}">{{ link }}</a></p>
{% endif %}
{% endblock %}
{% block subtitle %}
<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 %}
{% block body %}
<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.full_name }}</td>
<td style="text-align: center;">{{ part.total_stock }}</td>
<td style="text-align: center;">{{ part.minimum_stock }}</td>
</tr>
{% endblock %}

View File

@ -1,7 +1,14 @@
<div class='panel panel-hidden' id='panel-{% block label %}name{% endblock %}'> <div class='panel panel-hidden' id='panel-{% block label %}name{% endblock %}'>
{% block panel_heading %} {% block panel_heading %}
<div class='panel-heading'> <div class='panel-heading'>
<h4>{% block heading %}HEADING{% endblock %}</h4> <div class='d-flex flex-wrap'>
<h4>{% block heading %}HEADING{% endblock %}</h4>
{% include "spacer.html" %}
<div class='btn-group' role='group'>
{% block actions %}
{% endblock %}
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block panel_content %} {% block panel_content %}