Merge remote-tracking branch 'inventree/master' into stock-item-forms

This commit is contained in:
Oliver 2021-11-02 19:25:11 +11:00
commit 8725837460
19 changed files with 349 additions and 130 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

@ -142,7 +142,7 @@
} }
.navbar-spacer { .navbar-spacer {
height: 40px; height: 60px;
} }
#navbar-barcode-li { #navbar-barcode-li {

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

@ -17,7 +17,7 @@
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-info-circle" %} {% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" 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_MAIL_REQUIRED" icon="fa-info-circle" %}
<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>
@ -39,61 +39,81 @@
</table> </table>
<div class='panel-heading'> <div class='panel-heading'>
<h4>{% trans "Email" %}</h4> <div class='d-flex flex-span'>
<h4>{% trans "Email" %}</h4>
{% include "spacer.html" %}
</div>
</div> </div>
<div> <div class='row'>
{% if user.emailaddress_set.all %} <div class='col-sm-6'>
<p>{% trans 'The following email addresses are associated with your account:' %}</p> {% if user.emailaddress_set.all %}
<p>{% trans 'The following email addresses are associated with your account:' %}</p>
<form action="{% url 'account_email' %}" class="email_list" method="post"> <form action="{% url 'account_email' %}" class="email_list" method="post">
{% csrf_token %} {% csrf_token %}
<fieldset class="blockLabels"> <fieldset class="blockLabels">
{% for emailaddress in user.emailaddress_set.all %} {% for emailaddress in user.emailaddress_set.all %}
<div class="ctrlHolder"> <div>
<label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}"> <div class="ctrlHolder">
<label for="email_radio_{{forloop.counter}}" 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.verified %} {% if emailaddress.primary %}
<span class="verified">{% trans "Verified" %}</span> <b>{{ emailaddress.email }}</b>
{% 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 %}
<div class="buttonHolder"> <div class="buttonHolder">
<button class="btn btn-primary secondaryAction" type="submit" name="action_primary" >{% trans 'Make Primary' %}</button> <button class="btn btn-primary secondaryAction" type="submit" name="action_primary" >{% trans 'Make Primary' %}</button>
<button class="btn btn-primary secondaryAction" type="submit" name="action_send" {% if not mail_conf %}disabled{% endif %}>{% trans 'Re-send Verification' %}</button> <button class="btn btn-primary secondaryAction" type="submit" name="action_send" {% if not mail_conf %}disabled{% endif %}>{% trans 'Re-send Verification' %}</button>
<button class="btn btn-primary primaryAction" type="submit" name="action_remove" >{% trans 'Remove' %}</button> <button class="btn btn-primary primaryAction" type="submit" name="action_remove" >{% trans 'Remove' %}</button>
</div> </div>
</fieldset> </fieldset>
</form> </form>
{% else %} {% else %}
<p><strong>{% trans 'Warning:'%}</strong> <p><strong>{% trans 'Warning:'%}</strong>
{% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %} {% trans "You currently do not have any email address set up. You should really add an email address so you can receive notifications, reset your password, etc." %}
</p> </p>
{% endif %} {% endif %}
</div>
{% if can_add_email %} <div class='col-sm-6'>
<br> {% if can_add_email %}
<h4>{% trans "Add Email Address" %}</h4> <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">
{% csrf_token %} {% csrf_token %}
{{ add_email_form|crispy }}
<button class="btn btn-primary" name="action_add" type="submit">{% trans "Add Email" %}</button> <label for="id_email" class=" requiredField">
E-mail<span class="asteriskField">*</span>
</label>
<div id="div_id_email" class="form-group input-group mb-3">
<div class='input-group-prepend'><span class='input-group-text'>@</span></div>
<input type="email" name="email" placeholder='{% trans "Enter e-mail address" %}' class="textinput textInput form-control" required="" id="id_email">
<div class='input-group-append'>
<button class="btn btn-primary" name="action_add" type="submit">{% trans "Add Email" %}</button>
</div>
</div>
</form> </form>
{% endif %} {% endif %}
<br> </div>
</div> </div>
<div class='panel-heading'> <div class='panel-heading'>
@ -135,7 +155,9 @@
</form> </form>
{% else %} {% else %}
<p>{% trans 'You currently have no social network accounts connected to this account.' %}</p> <div class='alert alert-block alert-warning'>
{% trans "There are no social network accounts connected to your InvenTree account" %}
</div>
{% endif %} {% endif %}
<br> <br>
@ -155,26 +177,26 @@
<div class='row'> <div class='row'>
<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='panel-heading'> <div class='panel-heading'>
@ -186,7 +208,10 @@
<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;"><div id="div_id_language" class="form-group"><div class="controls "> <label for='language' class=' requiredField'>
{% trans "Select language" %}
</label>
<div class='form-group input-group mb-3'>
<select name="language" class="select form-control"> <select name="language" class="select form-control">
{% get_current_language as LANGUAGE_CODE %} {% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %} {% get_available_languages as LANGUAGES %}
@ -204,11 +229,11 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
</div></div></div> <div class='input-group-append'>
<div class="col-sm-6" style="width: auto;"> <input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary"> </div>
</div> </div>
</form> </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 %}