mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 07:05:41 +00:00 
			
		
		
		
	Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue2201
This commit is contained in:
		@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -102,9 +102,9 @@ class APITests(InvenTreeAPITestCase):
 | 
			
		||||
 | 
			
		||||
    fixtures = [
 | 
			
		||||
        'location',
 | 
			
		||||
        'stock',
 | 
			
		||||
        'part',
 | 
			
		||||
        'category',
 | 
			
		||||
        'part',
 | 
			
		||||
        'stock'
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    token = None
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -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
									
								
							
							
						
						
									
										58
									
								
								InvenTree/part/tasks.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
        )
 | 
			
		||||
@@ -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):
 | 
			
		||||
    """
 | 
			
		||||
 
 | 
			
		||||
@@ -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>')
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
@@ -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" %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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'>
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
 
 | 
			
		||||
@@ -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>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								InvenTree/templates/email/email.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								InvenTree/templates/email/email.html
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										29
									
								
								InvenTree/templates/email/low_stock_notification.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								InvenTree/templates/email/low_stock_notification.html
									
									
									
									
									
										Normal 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 %}
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user