mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	Merge branch 'master' of https://github.com/inventree/InvenTree into plugin-2037
This commit is contained in:
		| @@ -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. | ||||||
|   | |||||||
| @@ -141,6 +141,10 @@ | |||||||
|     float: left; |     float: left; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .navbar-spacer { | ||||||
|  |     height: 60px; | ||||||
|  | } | ||||||
|  |  | ||||||
| #navbar-barcode-li { | #navbar-barcode-li { | ||||||
|     border-left: none; |     border-left: none; | ||||||
|     border-right: none; |     border-right: none; | ||||||
| @@ -545,7 +549,6 @@ | |||||||
| .inventree-body { | .inventree-body { | ||||||
|     width: 100%; |     width: 100%; | ||||||
|     padding: 5px; |     padding: 5px; | ||||||
|     margin-top: 10px; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| .inventree-pre-content { | .inventree-pre-content { | ||||||
| @@ -562,10 +565,6 @@ | |||||||
|     transition: 0.1s; |     transition: 0.1s; | ||||||
| } | } | ||||||
|  |  | ||||||
| .body { |  | ||||||
|     padding-top: 50px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| .modal { | .modal { | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   z-index: 9999; |   z-index: 9999; | ||||||
|   | |||||||
| @@ -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 | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -102,9 +102,9 @@ class APITests(InvenTreeAPITestCase): | |||||||
|  |  | ||||||
|     fixtures = [ |     fixtures = [ | ||||||
|         'location', |         'location', | ||||||
|         'stock', |  | ||||||
|         'part', |  | ||||||
|         'category', |         'category', | ||||||
|  |         'part', | ||||||
|  |         'stock' | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     token = None |     token = None | ||||||
|   | |||||||
| @@ -142,7 +142,7 @@ | |||||||
|                         <td><span class='fas fa-calendar-alt'></span></td> |                         <td><span class='fas fa-calendar-alt'></span></td> | ||||||
|                         <td>{% trans "Completed" %}</td> |                         <td>{% trans "Completed" %}</td> | ||||||
|                         {% if build.completion_date %} |                         {% if build.completion_date %} | ||||||
|                         <td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge'>{{ build.completed_by }}</span>{% endif %}</td> |                         <td>{{ build.completion_date }}{% if build.completed_by %}<span class='badge badge-right rounded-pill bg-dark'>{{ build.completed_by }}</span>{% endif %}</td> | ||||||
|                         {% else %} |                         {% else %} | ||||||
|                         <td><em>{% trans "Build not complete" %}</em></td> |                         <td><em>{% trans "Build not complete" %}</em></td> | ||||||
|                         {% endif %} |                         {% endif %} | ||||||
|   | |||||||
| @@ -830,19 +830,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, | ||||||
|         }, |         }, | ||||||
|   | |||||||
| @@ -123,7 +123,7 @@ src="{% static 'img/blank_image.png' %}" | |||||||
|     <tr> |     <tr> | ||||||
|         <td><span class='fas fa-calendar-alt'></span></td> |         <td><span class='fas fa-calendar-alt'></span></td> | ||||||
|         <td>{% trans "Created" %}</td> |         <td>{% trans "Created" %}</td> | ||||||
|         <td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td> |         <td>{{ order.creation_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td> | ||||||
|     </tr> |     </tr> | ||||||
|     {% if order.issue_date %} |     {% if order.issue_date %} | ||||||
|     <tr> |     <tr> | ||||||
| @@ -143,7 +143,7 @@ src="{% static 'img/blank_image.png' %}" | |||||||
|     <tr> |     <tr> | ||||||
|         <td><span class='fas fa-calendar-alt'></span></td> |         <td><span class='fas fa-calendar-alt'></span></td> | ||||||
|         <td>{% trans "Received" %}</td> |         <td>{% trans "Received" %}</td> | ||||||
|         <td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td> |         <td>{{ order.complete_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.received_by }}</span></td> | ||||||
|     </tr> |     </tr> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {% if order.responsible %} |     {% if order.responsible %} | ||||||
|   | |||||||
| @@ -128,7 +128,7 @@ src="{% static 'img/blank_image.png' %}" | |||||||
|     <tr> |     <tr> | ||||||
|         <td><span class='fas fa-calendar-alt'></span></td> |         <td><span class='fas fa-calendar-alt'></span></td> | ||||||
|         <td>{% trans "Created" %}</td> |         <td>{% trans "Created" %}</td> | ||||||
|         <td>{{ order.creation_date }}<span class='badge'>{{ order.created_by }}</span></td> |         <td>{{ order.creation_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.created_by }}</span></td> | ||||||
|     </tr> |     </tr> | ||||||
|     {% if order.target_date %} |     {% if order.target_date %} | ||||||
|     <tr> |     <tr> | ||||||
| @@ -141,14 +141,14 @@ src="{% static 'img/blank_image.png' %}" | |||||||
|     <tr> |     <tr> | ||||||
|         <td><span class='fas fa-truck'></span></td> |         <td><span class='fas fa-truck'></span></td> | ||||||
|         <td>{% trans "Shipped" %}</td> |         <td>{% trans "Shipped" %}</td> | ||||||
|         <td>{{ order.shipment_date }}<span class='badge'>{{ order.shipped_by }}</span></td> |         <td>{{ order.shipment_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.shipped_by }}</span></td> | ||||||
|     </tr> |     </tr> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {% if order.status == PurchaseOrderStatus.COMPLETE %} |     {% if order.status == PurchaseOrderStatus.COMPLETE %} | ||||||
|     <tr> |     <tr> | ||||||
|         <td><span class='fas fa-calendar-alt'></span></td> |         <td><span class='fas fa-calendar-alt'></span></td> | ||||||
|         <td>{% trans "Received" %}</td> |         <td>{% trans "Received" %}</td> | ||||||
|         <td>{{ order.complete_date }}<span class='badge'>{{ order.received_by }}</span></td> |         <td>{{ order.complete_date }}<span class='badge badge-right rounded-pill bg-dark'>{{ order.received_by }}</span></td> | ||||||
|     </tr> |     </tr> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     {% if order.responsible %} |     {% if order.responsible %} | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										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 | ||||||
|  |         ) | ||||||
| @@ -8,18 +8,15 @@ | |||||||
| {% include "sidebar_link.html" with url=url text="Return to BOM" icon="fa-undo" %} | {% include "sidebar_link.html" with url=url text="Return to BOM" icon="fa-undo" %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
| {% block page_content %} | {% block heading %} | ||||||
|  | {% trans "Upload Bill of Materials" %} | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
| <div class='panel' id='panel-upload-file'> | {% block actions %} | ||||||
|     <div class='panel-heading'> | {% endblock %} | ||||||
|         {% block heading %} |  | ||||||
|         <h4>{% trans "Upload Bill of Materials" %}</h4> |  | ||||||
|         {{ wizard.form.media }} |  | ||||||
|         {% endblock %} |  | ||||||
|     </div> |  | ||||||
|     <div class='panel-content'> |  | ||||||
|         {% block details %} |  | ||||||
|  |  | ||||||
|  | {% block page_info %} | ||||||
|  | <div class='panel-content'> | ||||||
|     <p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %} |     <p>{% blocktrans with step=wizard.steps.step1 count=wizard.steps.count %}Step {{step}} of {{count}}{% endblocktrans %} | ||||||
|     {% if description %}- {{ description }}{% endif %}</p> |     {% if description %}- {{ description }}{% endif %}</p> | ||||||
|  |  | ||||||
| @@ -54,12 +51,12 @@ | |||||||
|     <button type="submit" class="save btn btn-outline-secondary">{% trans "Upload File" %}</button> |     <button type="submit" class="save btn btn-outline-secondary">{% trans "Upload File" %}</button> | ||||||
|     </form> |     </form> | ||||||
|     {% endblock form_buttons_bottom %} |     {% endblock form_buttons_bottom %} | ||||||
|  | </div> | ||||||
|         {% endblock details %} | {% endblock page_info %} | ||||||
|     </div> |  | ||||||
|  |  | ||||||
| {% endblock page_content %} |  | ||||||
|  |  | ||||||
| {% block js_ready %} | {% block js_ready %} | ||||||
| {{ block.super }} | {{ block.super }} | ||||||
|  |  | ||||||
|  | enableSidebar('bom-upload'); | ||||||
|  |  | ||||||
| {% endblock js_ready %} | {% endblock js_ready %} | ||||||
|   | |||||||
| @@ -210,7 +210,8 @@ | |||||||
|                 {% else %} |                 {% else %} | ||||||
|                 parent: null, |                 parent: null, | ||||||
|                 {% endif %} |                 {% endif %} | ||||||
|             } |             }, | ||||||
|  |             allowTreeView: true, | ||||||
|         } |         } | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,13 +20,6 @@ | |||||||
|         <!-- Details Table --> |         <!-- Details Table --> | ||||||
|         <table class="table table-striped table-condensed"> |         <table class="table table-striped table-condensed"> | ||||||
|             <col width='25'> |             <col width='25'> | ||||||
|             {% if part.IPN %} |  | ||||||
|             <tr> |  | ||||||
|                 <td><span class='fas fa-tag'></span></td> |  | ||||||
|                 <td>{% trans "IPN" %}</td> |  | ||||||
|                 <td>{{ part.IPN }}{% include "clip.html"%}</td> |  | ||||||
|             </tr> |  | ||||||
|             {% endif %} |  | ||||||
|             <tr> |             <tr> | ||||||
|                 <td><span class='fas fa-shapes'></span></td> |                 <td><span class='fas fa-shapes'></span></td> | ||||||
|                 <td>{% trans "Name" %}</td> |                 <td>{% trans "Name" %}</td> | ||||||
| @@ -37,6 +30,22 @@ | |||||||
|                 <td>{% trans "Description" %}</td> |                 <td>{% trans "Description" %}</td> | ||||||
|                 <td>{{ part.description }}{% include "clip.html"%}</td> |                 <td>{{ part.description }}{% include "clip.html"%}</td> | ||||||
|             </tr> |             </tr> | ||||||
|  |             {% if part.category %} | ||||||
|  |             <tr> | ||||||
|  |                 <td><span class='fas fa-sitemap'></span></td> | ||||||
|  |                 <td>{% trans "Category" %}</td> | ||||||
|  |                 <td> | ||||||
|  |                     <a href='{% url "category-detail" part.category.pk %}'>{{ part.category }}</a> | ||||||
|  |                 </td> | ||||||
|  |             </tr> | ||||||
|  |             {% endif %} | ||||||
|  |             {% if part.IPN %} | ||||||
|  |             <tr> | ||||||
|  |                 <td><span class='fas fa-tag'></span></td> | ||||||
|  |                 <td>{% trans "IPN" %}</td> | ||||||
|  |                 <td>{{ part.IPN }}{% include "clip.html"%}</td> | ||||||
|  |             </tr> | ||||||
|  |             {% endif %} | ||||||
|             {% if part.revision %} |             {% if part.revision %} | ||||||
|             <tr> |             <tr> | ||||||
|                 <td><span class='fas fa-code-branch'></span></td> |                 <td><span class='fas fa-code-branch'></span></td> | ||||||
| @@ -44,6 +53,20 @@ | |||||||
|                 <td>{{ part.revision }}{% include "clip.html"%}</td> |                 <td>{{ part.revision }}{% include "clip.html"%}</td> | ||||||
|             </tr> |             </tr> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|  |             {% if part.units %} | ||||||
|  |             <tr> | ||||||
|  |                 <td></td> | ||||||
|  |                 <td>{% trans "Units" %}</td> | ||||||
|  |                 <td>{{ part.units }}</td> | ||||||
|  |             </tr> | ||||||
|  |             {% endif %} | ||||||
|  |             {% if part.minimum_stock %} | ||||||
|  |             <tr> | ||||||
|  |                 <td><span class='fas fa-less-than-equal'></span></td> | ||||||
|  |                 <td>{% trans "Minimum stock level" %}</td> | ||||||
|  |                 <td>{{ part.minimum_stock }}</td> | ||||||
|  |             </tr> | ||||||
|  |             {% endif %} | ||||||
|             {% if part.keywords %} |             {% if part.keywords %} | ||||||
|             <tr> |             <tr> | ||||||
|                 <td><span class='fas fa-key'></span></td> |                 <td><span class='fas fa-key'></span></td> | ||||||
| @@ -64,7 +87,7 @@ | |||||||
|                 <td> |                 <td> | ||||||
|                     {{ part.creation_date }} |                     {{ part.creation_date }} | ||||||
|                     {% if part.creation_user %} |                     {% if part.creation_user %} | ||||||
|                     <span class='badge'>{{ part.creation_user }}</span> |                     <span class='badge badge-right rounded-pill bg-dark'>{{ part.creation_user }}</span> | ||||||
|                     {% endif %} |                     {% endif %} | ||||||
|                 </td> |                 </td> | ||||||
|             </tr> |             </tr> | ||||||
| @@ -79,7 +102,9 @@ | |||||||
|             <tr> |             <tr> | ||||||
|                 <td><span class='fas fa-search-location'></span></td> |                 <td><span class='fas fa-search-location'></span></td> | ||||||
|                 <td>{% trans "Default Location" %}</td> |                 <td>{% trans "Default Location" %}</td> | ||||||
|                 <td>{{ part.default_location }}</td> |                 <td> | ||||||
|  |                     <a href='{% url "stock-location-detail" part.default_location.pk %}'>{{ part.default_location }}</a> | ||||||
|  |                 </td> | ||||||
|             </tr> |             </tr> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|             {% if part.default_supplier %} |             {% if part.default_supplier %} | ||||||
|   | |||||||
| @@ -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): | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -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>') | ||||||
|   | |||||||
| @@ -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. | ||||||
|   | |||||||
| @@ -393,7 +393,7 @@ | |||||||
|         <td><span class='fas fa-calendar-alt'></span></td> |         <td><span class='fas fa-calendar-alt'></span></td> | ||||||
|         <td>{% trans "Last Stocktake" %}</td> |         <td>{% trans "Last Stocktake" %}</td> | ||||||
|         {% if item.stocktake_date %} |         {% if item.stocktake_date %} | ||||||
|         <td>{{ item.stocktake_date }} <span class='badge'>{{ item.stocktake_user }}</span></td> |         <td>{{ item.stocktake_date }} <span class='badge badge-right rounded-pill bg-dark'>{{ item.stocktake_user }}</span></td> | ||||||
|         {% else %} |         {% else %} | ||||||
|         <td><em>{% trans "No stocktake performed" %}</em></td> |         <td><em>{% trans "No stocktake performed" %}</em></td> | ||||||
|         {% endif %} |         {% endif %} | ||||||
|   | |||||||
| @@ -183,7 +183,8 @@ | |||||||
|             {% else %} |             {% else %} | ||||||
|             parent: 'null', |             parent: 'null', | ||||||
|             {% endif %} |             {% endif %} | ||||||
|         } |         }, | ||||||
|  |         allowTreeView: true, | ||||||
|     }); |     }); | ||||||
|      |      | ||||||
|     linkButtonsToSelection( |     linkButtonsToSelection( | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ If this location is deleted, these items will be moved to the top level 'Stock' | |||||||
|  |  | ||||||
| <ul class='list-group'> | <ul class='list-group'> | ||||||
|     {% for item in location.stock_items.all %} |     {% for item in location.stock_items.all %} | ||||||
|     <li class='list-group-item'><strong>{{ item.part.full_name }}</strong> - <em>{{ item.part.description }}</em><span class='badge'>{% decimal item.quantity %}</span></li> |     <li class='list-group-item'><strong>{{ item.part.full_name }}</strong> - <em>{{ item.part.description }}</em><span class='badge badge-right rounded-pill bg-dark'>{% decimal item.quantity %}</span></li> | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
| </ul> | </ul> | ||||||
| {% endif %} | {% endif %} | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ | |||||||
| {% inventree_title %} | {% trans "Index" %} | {% inventree_title %} | {% trans "Index" %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block breadcrumb_list %} | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
| {% block sidebar %} | {% block sidebar %} | ||||||
| <!-- Sidebar data is filled dynamically for the index page--> | <!-- Sidebar data is filled dynamically for the index page--> | ||||||
|   | |||||||
| @@ -8,6 +8,9 @@ | |||||||
| {% inventree_title %} | {% trans "Search Results" %} | {% inventree_title %} | {% trans "Search Results" %} | ||||||
| {% endblock %} | {% endblock %} | ||||||
|  |  | ||||||
|  | {% block breadcrumb_list %} | ||||||
|  | {% endblock %} | ||||||
|  |  | ||||||
| {% block content %} | {% block content %} | ||||||
|  |  | ||||||
| <div class='panel panel-inventree'> | <div class='panel panel-inventree'> | ||||||
|   | |||||||
| @@ -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> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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" %} | ||||||
|   | |||||||
| @@ -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'> | ||||||
|  |         <h4>{% trans "Part Import" %}</h4> | ||||||
|  |         {% include "spacer.html" %} | ||||||
|  |         <div class='btn-group' role='group'> | ||||||
|             <button class='btn btn-success' id='import-part'> |             <button class='btn btn-success' id='import-part'> | ||||||
|                 <span class='fas fa-plus-circle'></span> {% trans "Import Part" %} |                 <span class='fas fa-plus-circle'></span> {% trans "Import Part" %} | ||||||
|             </button>         |             </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'> | ||||||
|   | |||||||
| @@ -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 %} | ||||||
|   | |||||||
| @@ -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,10 +39,14 @@ | |||||||
| </table> | </table> | ||||||
|  |  | ||||||
| <div class='panel-heading'> | <div class='panel-heading'> | ||||||
|  |     <div class='d-flex flex-span'> | ||||||
|         <h4>{% trans "Email" %}</h4> |         <h4>{% trans "Email" %}</h4> | ||||||
|  |         {% include "spacer.html" %} | ||||||
|  |     </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div> | <div class='row'> | ||||||
|  |     <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> | ||||||
|  |  | ||||||
| @@ -51,19 +55,25 @@ | |||||||
|             <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}}" 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}}"/> |                         <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 %} | ||||||
|  |  | ||||||
| @@ -82,18 +92,28 @@ | |||||||
|         </p> |         </p> | ||||||
|  |  | ||||||
|         {% endif %} |         {% endif %} | ||||||
|  |     </div> | ||||||
|  |     <div class='col-sm-6'> | ||||||
|         {% if can_add_email %} |         {% if can_add_email %} | ||||||
|         <br> |         <h5>{% trans "Add Email Address" %}</h5> | ||||||
|         <h4>{% trans "Add Email Address" %}</h4> |  | ||||||
|  |  | ||||||
|         <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 }} |  | ||||||
|  |             <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> |                     <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'> | ||||||
|  |  | ||||||
|  |     <div class='col-sm-6'> | ||||||
|         <form action='{% url "settings-appearance" %}' method='post'> |         <form action='{% url "settings-appearance" %}' 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='theme' class=' requiredField'> | ||||||
|             <div id="div_id_themes" class="form-group"> |                 {% trans "Select theme" %} | ||||||
|                 <div class="controls "> |             </label> | ||||||
|                     <select name='theme' class='select form-control'> |             <div class='form-group input-group mb-3'> | ||||||
|  |                 <select id='theme' name='theme' class='select form-control'> | ||||||
|                     {% get_available_themes as themes %} |                     {% get_available_themes as themes %} | ||||||
|                     {% for theme in themes %} |                     {% for theme in themes %} | ||||||
|                     <option value='{{ theme.key }}'>{{ theme.name }}</option> |                     <option value='{{ theme.key }}'>{{ theme.name }}</option> | ||||||
|                     {% endfor %} |                     {% endfor %} | ||||||
|                 </select> |                 </select> | ||||||
|  |                 <div class='input-group-append'> | ||||||
|  |                     <input type="submit" value="{% trans 'Set Theme' %}" class="btn btn-primary"> | ||||||
|                 </div> |                 </div> | ||||||
|             </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> | ||||||
|  |  | ||||||
| <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,10 +229,10 @@ | |||||||
|                         </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"> | ||||||
|   | |||||||
							
								
								
									
										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 %} | ||||||
| @@ -1133,8 +1133,10 @@ function loadPartTable(table, url, options={}) { | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /* | ||||||
|  |  * Display a table of part categories | ||||||
|  |  */ | ||||||
| function loadPartCategoryTable(table, options) { | function loadPartCategoryTable(table, options) { | ||||||
|     /* Display a table of part categories */ |  | ||||||
|  |  | ||||||
|     var params = options.params || {}; |     var params = options.params || {}; | ||||||
|  |  | ||||||
| @@ -1157,15 +1159,15 @@ function loadPartCategoryTable(table, options) { | |||||||
|  |  | ||||||
|     setupFilterList(filterKey, table, filterListElement); |     setupFilterList(filterKey, table, filterListElement); | ||||||
|  |  | ||||||
|     var tree_view = inventreeLoad('category-tree-view') == 1; |     var tree_view = options.allowTreeView && inventreeLoad('category-tree-view') == 1; | ||||||
|  |  | ||||||
|     table.inventreeTable({ |     table.inventreeTable({ | ||||||
|         treeEnable: tree_view, |         treeEnable: tree_view, | ||||||
|         rootParentId: options.params.parent, |         rootParentId: tree_view ? options.params.parent : null, | ||||||
|         uniqueId: 'pk', |         uniqueId: 'pk', | ||||||
|         idField: 'pk', |         idField: 'pk', | ||||||
|         treeShowField: 'name', |         treeShowField: 'name', | ||||||
|         parentIdField: 'parent', |         parentIdField: tree_view ? 'parent' : null, | ||||||
|         method: 'get', |         method: 'get', | ||||||
|         url: options.url || '{% url "api-part-category-list" %}', |         url: options.url || '{% url "api-part-category-list" %}', | ||||||
|         queryParams: filters, |         queryParams: filters, | ||||||
| @@ -1176,7 +1178,7 @@ function loadPartCategoryTable(table, options) { | |||||||
|         name: 'category', |         name: 'category', | ||||||
|         original: original, |         original: original, | ||||||
|         showColumns: true, |         showColumns: true, | ||||||
|         buttons: [ |         buttons: options.allowTreeView ? [ | ||||||
|             { |             { | ||||||
|                 icon: 'fas fa-bars', |                 icon: 'fas fa-bars', | ||||||
|                 attributes: { |                 attributes: { | ||||||
| @@ -1215,9 +1217,11 @@ function loadPartCategoryTable(table, options) { | |||||||
|                     ); |                     ); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         ], |         ] : [], | ||||||
|         onPostBody: function() { |         onPostBody: function() { | ||||||
|  |  | ||||||
|  |             if (options.allowTreeView) { | ||||||
|  |  | ||||||
|                 tree_view = inventreeLoad('category-tree-view') == 1; |                 tree_view = inventreeLoad('category-tree-view') == 1; | ||||||
|  |  | ||||||
|                 if (tree_view) { |                 if (tree_view) { | ||||||
| @@ -1238,6 +1242,7 @@ function loadPartCategoryTable(table, options) { | |||||||
|                     $('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary'); |                     $('#view-category-tree').removeClass('btn-secondary').addClass('btn-outline-secondary'); | ||||||
|                     $('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary'); |                     $('#view-category-list').removeClass('btn-outline-secondary').addClass('btn-secondary'); | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|         }, |         }, | ||||||
|         columns: [ |         columns: [ | ||||||
|             { |             { | ||||||
|   | |||||||
| @@ -1416,8 +1416,11 @@ function loadStockTable(table, options) { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | /*  | ||||||
|  |  * Display a table of stock locations | ||||||
|  |  */ | ||||||
| function loadStockLocationTable(table, options) { | function loadStockLocationTable(table, options) { | ||||||
|     /* Display a table of stock locations */ |  | ||||||
|  |  | ||||||
|     var params = options.params || {}; |     var params = options.params || {}; | ||||||
|  |  | ||||||
| @@ -1443,15 +1446,15 @@ function loadStockLocationTable(table, options) { | |||||||
|         filters[key] = params[key]; |         filters[key] = params[key]; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     var tree_view = inventreeLoad('location-tree-view') == 1; |     var tree_view = options.allowTreeView && inventreeLoad('location-tree-view') == 1; | ||||||
|  |  | ||||||
|     table.inventreeTable({ |     table.inventreeTable({ | ||||||
|         treeEnable: tree_view, |         treeEnable: tree_view, | ||||||
|         rootParentId: options.params.parent, |         rootParentId: tree_view ? options.params.parent : null, | ||||||
|         uniqueId: 'pk', |         uniqueId: 'pk', | ||||||
|         idField: 'pk', |         idField: 'pk', | ||||||
|         treeShowField: 'name', |         treeShowField: 'name', | ||||||
|         parentIdField: 'parent', |         parentIdField: tree_view ? 'parent' : null, | ||||||
|         disablePagination: tree_view, |         disablePagination: tree_view, | ||||||
|         sidePagination: tree_view ? 'client' : 'server', |         sidePagination: tree_view ? 'client' : 'server', | ||||||
|         serverSort: !tree_view, |         serverSort: !tree_view, | ||||||
| @@ -1465,6 +1468,8 @@ function loadStockLocationTable(table, options) { | |||||||
|         showColumns: true, |         showColumns: true, | ||||||
|         onPostBody: function() { |         onPostBody: function() { | ||||||
|  |  | ||||||
|  |             if (options.allowTreeView) { | ||||||
|  |  | ||||||
|                 tree_view = inventreeLoad('location-tree-view') == 1; |                 tree_view = inventreeLoad('location-tree-view') == 1; | ||||||
|  |  | ||||||
|                 if (tree_view) { |                 if (tree_view) { | ||||||
| @@ -1485,8 +1490,9 @@ function loadStockLocationTable(table, options) { | |||||||
|                     $('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary'); |                     $('#view-location-tree').removeClass('btn-secondary').addClass('btn-outline-secondary'); | ||||||
|                     $('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary'); |                     $('#view-location-list').removeClass('btn-outline-secondary').addClass('btn-secondary'); | ||||||
|                 } |                 } | ||||||
|  |             } | ||||||
|         }, |         }, | ||||||
|         buttons: [ |         buttons: options.allowTreeView ? [ | ||||||
|             { |             { | ||||||
|                 icon: 'fas fa-bars', |                 icon: 'fas fa-bars', | ||||||
|                 attributes: { |                 attributes: { | ||||||
| @@ -1525,7 +1531,7 @@ function loadStockLocationTable(table, options) { | |||||||
|                     ); |                     ); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         ], |         ] : [], | ||||||
|         columns: [ |         columns: [ | ||||||
|             { |             { | ||||||
|                 checkbox: true, |                 checkbox: true, | ||||||
|   | |||||||
| @@ -12,7 +12,10 @@ | |||||||
|     <div class="navbar-header clearfix content-heading"> |     <div class="navbar-header clearfix content-heading"> | ||||||
|       <a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a> |       <a class="navbar-brand" id='logo' href="{% url 'index' %}" style="padding-top: 7px; padding-bottom: 5px;"><img src="{% static 'img/inventree.png' %}" width="32" height="32" style="display:block; margin: auto;"/></a> | ||||||
|     </div> |     </div> | ||||||
|     <div class="navbar-collapse collapse"> |     <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-objects" aria-label="Toggle navigation"> | ||||||
|  |       <span class="navbar-toggler-icon"></span> | ||||||
|  |     </button> | ||||||
|  |     <div class="navbar-collapse collapse" id="navbar-objects"> | ||||||
|       <ul class="navbar-nav"> |       <ul class="navbar-nav"> | ||||||
|         {% if roles.part.view %} |         {% if roles.part.view %} | ||||||
|         <li class='nav-item'> |         <li class='nav-item'> | ||||||
| @@ -130,3 +133,7 @@ | |||||||
|  |  | ||||||
|   </div> |   </div> | ||||||
| </nav> | </nav> | ||||||
|  |  | ||||||
|  | {% if sticky %} | ||||||
|  | <div class='navbar-spacer'></div> | ||||||
|  | {% endif %} | ||||||
|   | |||||||
| @@ -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'> | ||||||
|  |         <div class='d-flex flex-wrap'> | ||||||
|             <h4>{% block heading %}HEADING{% endblock %}</h4> |             <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 %} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user