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))
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():
"""
Return the qualified path for the 'blank image' placeholder.

View File

@ -52,7 +52,7 @@ def schedule_task(taskname, **kwargs):
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.
This is different to a 'scheduled' task,
@ -108,7 +108,7 @@ def offload_task(taskname, force_sync=False, *args, **kwargs):
return
# Workers are not running: run it as synchronous task
_func()
_func(*args, **kwargs)
def heartbeat():
@ -290,7 +290,7 @@ def update_exchange_rates():
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,
to the specified recipients list.
@ -306,4 +306,5 @@ def send_email(subject, body, recipients, from_email=None):
from_email,
recipients,
fail_silently=False,
html_message=html_message
)

View File

@ -102,9 +102,9 @@ class APITests(InvenTreeAPITestCase):
fixtures = [
'location',
'stock',
'part',
'category',
'part',
'stock'
]
token = None

View File

@ -807,19 +807,19 @@ class InvenTreeSetting(BaseInvenTreeSetting):
# login / SSO
'LOGIN_ENABLE_PWD_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,
'validator': bool,
},
'LOGIN_ENABLE_REG': {
'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,
'validator': bool,
},
'LOGIN_ENABLE_SSO': {
'name': _('Enable SSO'),
'description': _('Enable SSO on the login-pages'),
'description': _('Enable SSO on the login pages'),
'default': False,
'validator': bool,
},

View File

@ -1988,6 +1988,9 @@ class Part(MPTTModel):
def related_count(self):
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):
""" 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()
@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()
def python_version(*args, **kwargs):
"""

View File

@ -14,6 +14,8 @@ from stock.models import StockItem
from common.models import InvenTreeSetting
import InvenTree.helpers
register = template.Library()
@ -119,18 +121,10 @@ def internal_link(link, 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 not base_url:
if not url:
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>')

View File

@ -17,7 +17,7 @@ from django.db.models import Sum, Q
from django.db.models.functions import Coalesce
from django.core.validators import MinValueValidator
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 markdownx.models import MarkdownxField
@ -41,6 +41,7 @@ from users.models import Owner
from company import models as CompanyModels
from part import models as PartModels
from part import tasks as part_tasks
class StockLocation(InvenTreeTree):
@ -1651,6 +1652,24 @@ def before_delete_stock_item(sender, instance, using, **kwargs):
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):
"""
Model for storing file attachments against a StockItem object.

View File

@ -7,6 +7,12 @@
{% trans "Category Settings" %}
{% 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 %}
<div class='row'>
@ -21,12 +27,6 @@
</form>
</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>

View File

@ -13,29 +13,31 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="INVENTREE_DEFAULT_CURRENCY" icon="fa-globe" %}
</tbody>
</table>
<table class='table table-striped table-condensed'>
<tbody>
<tr>
<td></td>
<th>{% trans "Base Currency" %}</th>
<th>{{ base_currency }}</th>
</tr>
<tr>
<th colspan='2'>{% trans "Exchange Rates" %}</th>
<td></td>
<th colspan='4'>{% trans "Exchange Rates" %}</th>
</tr>
{% for rate in rates %}
<tr>
<td>{{ rate.currency }}</td>
<td></td>
<td>{{ rate.value }}</td>
<td>{{ rate.currency }}</td>
<td></td>
<td></td>
</tr>
{% endfor %}
<tr>
<th></th>
<th>
{% trans "Last Update" %}
</th>
<td>
<td colspan="3">
{% if rates_updated %}
{{ rates_updated }}
{% else %}
@ -44,7 +46,7 @@
<form action='{% url "settings-currencies-refresh" %}' method='post'>
<div id='refresh-rates-form'>
{% 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>
</form>
</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_ENFORCE_MFA" %}
<tr>
<td>{% trans 'Signup' %}</td>
<th><h5>{% trans 'Signup' %}</h5></th>
<td colspan='4'></td>
</tr>
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-info-circle" %}

View File

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

View File

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

View File

@ -11,18 +11,18 @@
{% trans "Account Settings" %}
{% 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 %}
{% 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'>
<tr>
<td>{% trans "Username" %}</td>
@ -40,10 +40,12 @@
<div class="row">
<div class='panel-heading'>
<div class='d-flex flex-span'>
<h4>{% trans "Email" %}</h4>
</div>
</div>
<div class="col-md-6">
<div class="col-sm-6">
{% if user.emailaddress_set.all %}
<p>{% trans 'The following email addresses are associated with your account:' %}</p>
@ -52,20 +54,26 @@
<fieldset class="blockLabels">
{% for emailaddress in user.emailaddress_set.all %}
<div>
<div class="ctrlHolder">
<label for="email_radio_{{forloop.counter}}"
class="{% if emailaddress.primary %}primary_email{%endif%}">
<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}}" />
{{ emailaddress.email }}
{% if emailaddress.verified %}
<span class="verified">{% trans "Verified" %}</span>
{% if emailaddress.primary %}
<b>{{ emailaddress.email }}</b>
{% else %}
<span class="unverified">{% trans "Unverified" %}</span>
{{ emailaddress.email }}
{% endif %}
{% if emailaddress.primary %}<span class="primary">{% trans "Primary" %}</span>{% endif %}
</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>
{% endfor %}
@ -88,7 +96,7 @@
</div>
{% if can_add_email %}
<div class="col-md-6">
<div class="col-sm-6">
<h5>{% trans "Add Email Address" %}</h5>
<form method="post" action="{% url 'account_email' %}" class="add_email">
@ -207,26 +215,26 @@
<h4>{% trans "Theme Settings" %}</h4>
</div>
<form action='{% url "settings-appearance" %}' method='post'>
{% csrf_token %}
<input name='next' type='hidden' value='{% url "settings" %}'>
<div class="col-sm-6" style="width: 200px;">
<div id="div_id_themes" class="form-group">
<div class="controls ">
<select name='theme' class='select form-control'>
{% get_available_themes as themes %}
{% for theme in themes %}
<option value='{{ theme.key }}'>{{ theme.name }}</option>
{% endfor %}
</select>
<div class='col-sm-6'>
<form action='{% url "settings-appearance" %}' method='post'>
{% csrf_token %}
<input name='next' type='hidden' value='{% url "settings" %}'>
<label for='theme' class=' requiredField'>
{% trans "Select theme" %}
</label>
<div class='form-group input-group mb-3'>
<select id='theme' name='theme' class='select form-control'>
{% get_available_themes as themes %}
{% for theme in themes %}
<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 class="col-sm-6" style="width: auto;">
<input type="submit" value="{% trans 'Set Theme' %}" class="btn btn btn-primary">
</div>
</form>
</form>
</div>
</div>
<div class="row">
@ -238,33 +246,32 @@
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<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 ">
<select name="language" class="select form-control">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
{% define language.code as lang_code %}
{% define locale_stats|keyvalue:lang_code as lang_translated %}
<option value="{{ lang_code }}" {% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
{{ language.name_local }} ({{ lang_code }})
{% if lang_translated %}
<label for='language' class=' requiredField'>
{% trans "Select language" %}
</label>
<div class='form-group input-group mb-3'>
<select name="language" class="select form-control">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
{% define language.code as lang_code %}
{% define locale_stats|keyvalue:lang_code as lang_translated %}
<option value="{{ lang_code }}"{% if lang_code == LANGUAGE_CODE %} selected{% endif %}>
{{ language.name_local }} ({{ lang_code }})
{% if lang_translated %}
{% blocktrans %}{{ lang_translated }}% translated{% endblocktrans %}
{% else %}
{% trans 'No translations available' %}
{% endif %}
</option>
{% endfor %}
</select>
</div>
{% endif %}
</option>
{% endfor %}
</select>
<div class='input-group-append'>
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
</div>
</div>
<div class="col-sm-6" style="width: auto;">
<input type="submit" value="{% trans 'Set Language' %}" class="btn btn btn-primary">
</div>
</form>
</form>
</div>
<div class="col-sm-6">
<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 %}'>
{% block 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>
{% endblock %}
{% block panel_content %}