From d2969d0235ee3a593725cb24e42f1871bdfcdf58 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 20 Mar 2022 19:19:42 +1100 Subject: [PATCH 01/89] Adds a per-user setting to allow sending of emails for notification events --- InvenTree/common/models.py | 7 +++++++ .../InvenTree/settings/settings.html | 1 + .../templates/InvenTree/settings/sidebar.html | 2 ++ .../settings/user_notifications.html | 20 +++++++++++++++++++ 4 files changed, 30 insertions(+) create mode 100644 InvenTree/templates/InvenTree/settings/user_notifications.html diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 56e41dd24b..54da688992 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -1193,6 +1193,13 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): 'validator': bool, }, + 'NOTIFICATION_SEND_EMAILS': { + 'name': _('Enable email notifications'), + 'description': _('Allow sending of emails for event notifications'), + 'default': True, + 'validator': bool, + }, + "LABEL_INLINE": { 'name': _('Inline label display'), 'description': _('Display PDF labels in the browser, instead of downloading as a file'), diff --git a/InvenTree/templates/InvenTree/settings/settings.html b/InvenTree/templates/InvenTree/settings/settings.html index ba27187747..dbb526cfd3 100644 --- a/InvenTree/templates/InvenTree/settings/settings.html +++ b/InvenTree/templates/InvenTree/settings/settings.html @@ -22,6 +22,7 @@ {% include "InvenTree/settings/user_settings.html" %} {% include "InvenTree/settings/user_homepage.html" %} {% include "InvenTree/settings/user_search.html" %} +{% include "InvenTree/settings/user_notifications.html" %} {% include "InvenTree/settings/user_labels.html" %} {% include "InvenTree/settings/user_reports.html" %} {% include "InvenTree/settings/user_display.html" %} diff --git a/InvenTree/templates/InvenTree/settings/sidebar.html b/InvenTree/templates/InvenTree/settings/sidebar.html index 85e0b4ce94..9df6b0736f 100644 --- a/InvenTree/templates/InvenTree/settings/sidebar.html +++ b/InvenTree/templates/InvenTree/settings/sidebar.html @@ -14,6 +14,8 @@ {% include "sidebar_item.html" with label='user-home' text=text icon="fa-home" %} {% trans "Search Settings" as text %} {% include "sidebar_item.html" with label='user-search' text=text icon="fa-search" %} +{% trans "Notifications" as text %} +{% include "sidebar_item.html" with label='user-notifications' text=text icon="fa-bell" %} {% trans "Label Printing" as text %} {% include "sidebar_item.html" with label='user-labels' text=text icon="fa-tag" %} {% trans "Reporting" as text %} diff --git a/InvenTree/templates/InvenTree/settings/user_notifications.html b/InvenTree/templates/InvenTree/settings/user_notifications.html new file mode 100644 index 0000000000..4e9889ca69 --- /dev/null +++ b/InvenTree/templates/InvenTree/settings/user_notifications.html @@ -0,0 +1,20 @@ +{% extends "panel.html" %} + +{% load i18n %} +{% load inventree_extras %} + +{% block label %}user-notifications{% endblock label %} + +{% block heading %}{% trans "Notification Settings" %}{% endblock heading %} + +{% block content %} + +
+ + + {% include "InvenTree/settings/setting.html" with key="NOTIFICATION_SEND_EMAILS" icon='fa-envelope' user_setting=True %} + +
+
+ +{% endblock content %} \ No newline at end of file From 5f98cdf3c751568ecad16614918e136b2b891954 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 20 Mar 2022 19:31:59 +1100 Subject: [PATCH 02/89] Email notifications are only sent for users who have the setting enabled --- InvenTree/common/notifications.py | 35 ++++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 6ba7b6d659..a2c0622f91 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -8,15 +8,19 @@ from allauth.account.models import EmailAddress from InvenTree.helpers import inheritors from InvenTree.ready import isImportingData from common.models import NotificationEntry, NotificationMessage +from common.models import InvenTreeUserSetting + import InvenTree.tasks logger = logging.getLogger('inventree') -# region notification classes -# region base classes class NotificationMethod: + """ + Base class for notification methods + """ + METHOD_NAME = '' CONTEXT_BUILTIN = ['name', 'message', ] CONTEXT_EXTRA = [] @@ -95,10 +99,8 @@ class SingleNotificationMethod(NotificationMethod): class BulkNotificationMethod(NotificationMethod): def send_bulk(self): raise NotImplementedError('The `send` method must be overriden!') -# endregion -# region implementations class EmailNotification(BulkNotificationMethod): METHOD_NAME = 'mail' CONTEXT_EXTRA = [ @@ -108,13 +110,26 @@ class EmailNotification(BulkNotificationMethod): ] def get_targets(self): + """ + Return a list of target email addresses, + only for users which allow email notifications + """ + + allowed_users = [] + + for user in self.targets: + allows_emails = InvenTreeUserSetting.get_setting('NOTIFICATION_SEND_EMAILS', user=user) + + if allows_emails: + allowed_users.append(user) + return EmailAddress.objects.filter( - user__in=self.targets, + user__in=allowed_users, ) def send_bulk(self): html_message = render_to_string(self.context['template']['html'], self.context) - targets = self.targets.values_list('email', flat=True) + targets = self.get_targets() InvenTree.tasks.send_email(self.context['template']['subject'], '', targets, html_message=html_message) @@ -137,20 +152,20 @@ class UIMessageNotification(SingleNotificationMethod): message=self.context['message'], ) return True -# endregion -# endregion def trigger_notifaction(obj, category=None, obj_ref='pk', targets=None, target_fnc=None, target_args=[], target_kwargs={}, context={}): """ - Send out an notification + Send out a notification """ - # check if data is importet currently + + # Check if data is importing currently if isImportingData(): return # Resolve objekt reference obj_ref_value = getattr(obj, obj_ref) + # Try with some defaults if not obj_ref_value: obj_ref_value = getattr(obj, 'pk') From 9cc2cc568301a379355d297218a3feb2adae430f Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 20 Mar 2022 19:36:52 +1100 Subject: [PATCH 03/89] Fix asynchronous order of events for "mark all as read" button --- InvenTree/templates/InvenTree/notifications/inbox.html | 2 +- .../templates/InvenTree/notifications/notifications.html | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/InvenTree/notifications/inbox.html b/InvenTree/templates/InvenTree/notifications/inbox.html index 7ec2a27b6a..d8f5190860 100644 --- a/InvenTree/templates/InvenTree/notifications/inbox.html +++ b/InvenTree/templates/InvenTree/notifications/inbox.html @@ -10,7 +10,7 @@ {% endblock %} {% block actions %} -
+
{% trans "Mark all as read" %}
diff --git a/InvenTree/templates/InvenTree/notifications/notifications.html b/InvenTree/templates/InvenTree/notifications/notifications.html index 89f10c3e3d..fedf8a1448 100644 --- a/InvenTree/templates/InvenTree/notifications/notifications.html +++ b/InvenTree/templates/InvenTree/notifications/notifications.html @@ -126,8 +126,12 @@ $("#mark-all").on('click', function() { { read: false, }, + { + success: function(response) { + updateNotificationTables(); + } + } ); - updateNotificationTables(); }); loadNotificationTable("#history-table", { From ace61c6aab8114b85ae7d0541afc9e79d7442d24 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 20 Mar 2022 19:38:40 +1100 Subject: [PATCH 04/89] Little bit of eye candy --- InvenTree/templates/js/translated/notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/notification.js b/InvenTree/templates/js/translated/notification.js index 4da4c2e875..a289dcb8f7 100644 --- a/InvenTree/templates/js/translated/notification.js +++ b/InvenTree/templates/js/translated/notification.js @@ -253,7 +253,7 @@ function openNotificationPanel() { { success: function(response) { if (response.length == 0) { - html = `

{% trans "No unread notifications" %}

`; + html = `

{% trans "No unread notifications" %}

`; } else { // build up items response.forEach(function(item, index) { From c8bbd25001d67149824dea1fa91ed3b2e78648bf Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 20 Mar 2022 19:41:42 +1100 Subject: [PATCH 05/89] Bug fix --- InvenTree/common/notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index a2c0622f91..9151f9b879 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -129,7 +129,7 @@ class EmailNotification(BulkNotificationMethod): def send_bulk(self): html_message = render_to_string(self.context['template']['html'], self.context) - targets = self.get_targets() + targets = self.get_targets().values_list('email', flat=True) InvenTree.tasks.send_email(self.context['template']['subject'], '', targets, html_message=html_message) From f92aa07f82c1dddcb0095f9cfdcc1f031d3377b6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 23 Mar 2022 12:01:50 +1100 Subject: [PATCH 06/89] Fix null values in build order serial number field --- InvenTree/templates/js/translated/build.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 5b416b4d22..46f7f32e42 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -213,7 +213,7 @@ function createBuildOutput(build_id, options) { success: function(data) { if (data.next) { fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${data.next}`; - } else { + } else if (data.latest) { fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`; } }, From b720c2e4318a2a70d71e8ad19613b075c4be321d Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 11:49:34 +1100 Subject: [PATCH 07/89] Check for empty string when rendering dates --- InvenTree/part/templatetags/inventree_extras.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index 3deab1ecd1..dd20d49e39 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -57,6 +57,13 @@ def render_date(context, date_object): return None if type(date_object) == str: + + date_object = date_object.strip() + + # Check for empty string + if len(date_object) == 0: + return None + # If a string is passed, first convert it to a datetime date_object = date.fromisoformat(date_object) From 31b71fe29fd9f0179eb733ec9f8bac3a5c60d080 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 11:56:39 +1100 Subject: [PATCH 08/89] Catch error when incorrect date format string is passed --- InvenTree/part/templatetags/inventree_extras.py | 10 +++++++++- InvenTree/plugin/events.py | 9 +++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/InvenTree/part/templatetags/inventree_extras.py b/InvenTree/part/templatetags/inventree_extras.py index dd20d49e39..dc93e00efa 100644 --- a/InvenTree/part/templatetags/inventree_extras.py +++ b/InvenTree/part/templatetags/inventree_extras.py @@ -8,6 +8,7 @@ over and above the built-in Django tags. from datetime import date, datetime import os import sys +import logging from django.utils.html import format_html @@ -31,6 +32,9 @@ from plugin.models import PluginSetting register = template.Library() +logger = logging.getLogger('inventree') + + @register.simple_tag() def define(value, *args, **kwargs): """ @@ -65,7 +69,11 @@ def render_date(context, date_object): return None # If a string is passed, first convert it to a datetime - date_object = date.fromisoformat(date_object) + try: + date_object = date.fromisoformat(date_object) + except ValueError: + logger.warning(f"Tried to convert invalid date string: {date_object}") + return None # We may have already pre-cached the date format by calling this already! user_date_format = context.get('user_date_format', None) diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index 049c8626c5..b75f5f92bb 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -59,15 +59,24 @@ def register_event(event, *args, **kwargs): logger.debug(f"Registering triggered event: '{event}'") + print("register_event") + # Determine if there are any plugins which are interested in responding if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'): + print("checking plugins") + with transaction.atomic(): for slug, plugin in registry.plugins.items(): + print("slug:", slug) + print("plugin:", plugin) + if plugin.mixin_enabled('events'): + print("events are enabled for this plugin!") + config = plugin.plugin_config() if config and config.active: From 66487817dd06c72eb73fc4c5848cef6550ed1136 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 12:03:16 +1100 Subject: [PATCH 09/89] Remove debug messages --- InvenTree/plugin/events.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index b75f5f92bb..f0e2458810 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -70,13 +70,8 @@ def register_event(event, *args, **kwargs): for slug, plugin in registry.plugins.items(): - print("slug:", slug) - print("plugin:", plugin) - if plugin.mixin_enabled('events'): - print("events are enabled for this plugin!") - config = plugin.plugin_config() if config and config.active: From bd0bf4368c4adc9bacc8a637247f13217cd732ee Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 12:08:56 +1100 Subject: [PATCH 10/89] Remove more debug messages --- InvenTree/plugin/events.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index f0e2458810..049c8626c5 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -59,13 +59,9 @@ def register_event(event, *args, **kwargs): logger.debug(f"Registering triggered event: '{event}'") - print("register_event") - # Determine if there are any plugins which are interested in responding if settings.PLUGIN_TESTING or InvenTreeSetting.get_setting('ENABLE_PLUGINS_EVENTS'): - print("checking plugins") - with transaction.atomic(): for slug, plugin in registry.plugins.items(): From 69e9d1625a1c01fa5e77d4190362a379dcca8c09 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 12:51:27 +1100 Subject: [PATCH 11/89] Adds a LabelPrintingMixin plugin class - Enables the implementation of custom label printing plugins - Will be available directly from the "print labels" dialog box --- .../plugin/builtin/integration/mixins.py | 53 +++++++++++++++++++ InvenTree/plugin/mixins/__init__.py | 4 +- 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 1e6b7e38b7..e379f974f1 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -393,6 +393,59 @@ class AppMixin: return True +class LabelPrintingMixin: + """ + Mixin which enables direct printing of stock labels. + + Each plugin should provide a PRINTER_NAME attribute, + and also implement the print_label() function + """ + + class MixinMeta: + """ + Meta options for this mixin + """ + MIXIN_NAME = 'Label printing' + + def __init__(self): + super().__init__() + self.add_mixin('labels', 'has_label_printing', __class__) + + @property + def has_label_printing(self): + + if not bool(self.PRINTER_NAME): + raise ValueError("PRINTER_NAME must be defined") + + return True + + PRINTER_NAME = "LabelPrinter" + + def get_printer_name(self): + return self.PRINTER_NAME + + def print_labels(self, labels, **kwargs): + """ + Print multiple labels. + Default implementation is to call print_label() for each label, + but it can be overridden if desired. + """ + for label in labels: + self.print_label(label, **kwargs) + + def print_label(self, label, **kwargs): + """ + Callback to print a single label + + Arguments: + label: A black-and-white pillow Image object + + """ + + # Unimplemented (to be implemented by the particular plugin class) + ... + + class APICallMixin: """ Mixin that enables easier API calls for a plugin diff --git a/InvenTree/plugin/mixins/__init__.py b/InvenTree/plugin/mixins/__init__.py index 8097b0b459..86e5e92f37 100644 --- a/InvenTree/plugin/mixins/__init__.py +++ b/InvenTree/plugin/mixins/__init__.py @@ -2,7 +2,8 @@ Utility class to enable simpler imports """ -from ..builtin.integration.mixins import APICallMixin, AppMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin +from ..builtin.integration.mixins import APICallMixin, AppMixin, LabelPrintingMixin, SettingsMixin, EventMixin, ScheduleMixin, UrlsMixin, NavigationMixin + from ..builtin.action.mixins import ActionMixin from ..builtin.barcode.mixins import BarcodeMixin @@ -10,6 +11,7 @@ __all__ = [ 'APICallMixin', 'AppMixin', 'EventMixin', + 'LabelPrintingMixin', 'NavigationMixin', 'ScheduleMixin', 'SettingsMixin', From 4e041e97b93e1ece316a77bbbe7c9896195aada0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 13:00:03 +1100 Subject: [PATCH 12/89] Catch potential IntegrityError when importing plugins --- InvenTree/plugin/registry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/registry.py b/InvenTree/plugin/registry.py index 2db1beb805..3276c82f45 100644 --- a/InvenTree/plugin/registry.py +++ b/InvenTree/plugin/registry.py @@ -10,12 +10,13 @@ import pathlib import logging import os import subprocess + from typing import OrderedDict from importlib import reload from django.apps import apps from django.conf import settings -from django.db.utils import OperationalError, ProgrammingError +from django.db.utils import OperationalError, ProgrammingError, IntegrityError from django.conf.urls import url, include from django.urls import clear_url_caches from django.contrib import admin @@ -282,6 +283,8 @@ class PluginsRegistry: if not settings.PLUGIN_TESTING: raise error # pragma: no cover plugin_db_setting = None + except (IntegrityError) as error: + logger.error(f"Error initializing plugin: {error}") # Always activate if testing if settings.PLUGIN_TESTING or (plugin_db_setting and plugin_db_setting.active): From 86b5655c5f6f3b0913275fe485fc90b0f0954531 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 13:15:57 +1100 Subject: [PATCH 13/89] Simplify new plugin class --- InvenTree/plugin/builtin/integration/mixins.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index e379f974f1..2d56e61929 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -397,8 +397,9 @@ class LabelPrintingMixin: """ Mixin which enables direct printing of stock labels. - Each plugin should provide a PRINTER_NAME attribute, - and also implement the print_label() function + Each plugin must provide a PLUGIN_NAME attribute, which is used to uniquely identify the printer. + + The plugin must also implement the print_label() function """ class MixinMeta: @@ -409,17 +410,7 @@ class LabelPrintingMixin: def __init__(self): super().__init__() - self.add_mixin('labels', 'has_label_printing', __class__) - - @property - def has_label_printing(self): - - if not bool(self.PRINTER_NAME): - raise ValueError("PRINTER_NAME must be defined") - - return True - - PRINTER_NAME = "LabelPrinter" + self.add_mixin('labels', True, __class__) def get_printer_name(self): return self.PRINTER_NAME From e62b6063bb6863992db82199208dbfc38b2a2fbe Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 13:20:26 +1100 Subject: [PATCH 14/89] If printing plugins are available, let the user select them when printing --- InvenTree/templates/js/translated/label.js | 92 ++++++++++++++++++---- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js index 1c843917e6..2e1a477387 100644 --- a/InvenTree/templates/js/translated/label.js +++ b/InvenTree/templates/js/translated/label.js @@ -57,13 +57,20 @@ function printStockItemLabels(items) { response, items, { - success: function(pk) { + success: function(data) { + + var pk = data.label; + var href = `/api/label/stock/${pk}/print/?`; items.forEach(function(item) { href += `items[]=${item}&`; }); + if (data.plugin) { + href += `plugin=${data.plugin}`; + } + window.location.href = href; } } @@ -107,13 +114,20 @@ function printStockLocationLabels(locations) { response, locations, { - success: function(pk) { + success: function(data) { + + var pk = data.label; + var href = `/api/label/location/${pk}/print/?`; locations.forEach(function(location) { href += `locations[]=${location}&`; }); + if (data.plugin) { + href += `plugin=${data.plugin}`; + } + window.location.href = href; } } @@ -162,13 +176,20 @@ function printPartLabels(parts) { response, parts, { - success: function(pk) { + success: function(data) { + + var pk = data.label; + var url = `/api/label/part/${pk}/print/?`; parts.forEach(function(part) { url += `parts[]=${part}&`; }); + if (data.plugin) { + href += `plugin=${data.plugin}`; + } + window.location.href = url; } } @@ -188,18 +209,53 @@ function selectLabel(labels, items, options={}) { * (via AJAX) from the server. */ - // If only a single label template is provided, - // just run with that! + // Array of available plugins for label printing + var plugins = []; - if (labels.length == 1) { - if (options.success) { - options.success(labels[0].pk); + // Request a list of available label printing plugins from the server + inventreeGet( + '{% url "api-plugin-list" %}', + { + + }, + { + async: false, + success: function(response) { + response.forEach(function(plugin) { + if (plugin.mixins && plugin.mixins.labels) { + // This plugin supports label printing + plugins.push(plugin); + } + }); + } } + ); - return; + var plugin_selection = ''; + + + if (plugins.length > 0) { + plugin_selection =` +
+ +
+ +
+
+ `; } - var modal = options.modal || '#modal-form'; var label_list = makeOptionsList( @@ -233,14 +289,15 @@ function selectLabel(labels, items, options={}) {
- ${label_list}
+ ${plugin_selection}
`; openModal({ @@ -255,14 +312,17 @@ function selectLabel(labels, items, options={}) { modalSubmit(modal, function() { - var label = $(modal).find('#id_label'); - - var pk = label.val(); + var label = $(modal).find('#id_label').val(); + var plugin = $(modal).find('#id_plugin').val(); closeModal(modal); if (options.success) { - options.success(pk); + options.success({ + // Return the selected label template and plugin + label: label, + plugin: plugin, + }); } }); } From f1f07a1977f85378b86164077a641423593502c6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 13:31:01 +1100 Subject: [PATCH 15/89] Extract plugin information from label printing request --- InvenTree/label/api.py | 36 ++++++++++++++++++++++ InvenTree/templates/js/translated/label.js | 3 +- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 475d4f8fea..99e51b48b4 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ + +from django.conf import settings from django.conf.urls import url, include from django.core.exceptions import ValidationError, FieldError from django.http import HttpResponse @@ -14,6 +16,8 @@ from rest_framework.response import Response import InvenTree.helpers import common.models +from plugin.registry import registry + from stock.models import StockItem, StockLocation from part.models import Part @@ -46,11 +50,43 @@ class LabelPrintMixin: Mixin for printing labels """ + def get_plugin(self, request): + """ + Return the label printing plugin associated with this request. + This is provided in the url, e.g. ?plugin=myprinter + + Requires: + - settings.PLUGINS_ENABLED is True + - matching plugin can be found + - matching plugin implements the 'labels' mixin + - matching plugin is enabled + """ + + if not settings.PLUGINS_ENABLED: + return None + + plugin_key = request.query_params.get('plugin', None) + + for slug, plugin in registry.plugins.items(): + + if slug == plugin_key and plugin.mixin_enabled('labels'): + + config = plugin.plugin_config() + + if config and config.active: + # Only return the plugin if it is enabled! + return plugin + + # No matches found + return None + def print(self, request, items_to_print): """ Print this label template against a number of pre-validated items """ + # Check the request to determine if the user has selected a label printing plugin + plugin = self.get_plugin(request) if len(items_to_print) == 0: # No valid items provided, return an error message data = { diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js index 2e1a477387..f91d9c2431 100644 --- a/InvenTree/templates/js/translated/label.js +++ b/InvenTree/templates/js/translated/label.js @@ -222,7 +222,8 @@ function selectLabel(labels, items, options={}) { async: false, success: function(response) { response.forEach(function(plugin) { - if (plugin.mixins && plugin.mixins.labels) { + // Look for active plugins which implement the 'labels' mixin class + if (plugin.active && plugin.mixins && plugin.mixins.labels) { // This plugin supports label printing plugins.push(plugin); } From 6c25a5805dc008fead599fa1e32b5285bad002f8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 14:57:01 +1100 Subject: [PATCH 16/89] Allow offloading of label printing to the configured plugin --- InvenTree/label/api.py | 52 ++++++++++++++++++- .../plugin/builtin/integration/mixins.py | 9 ---- InvenTree/plugin/events.py | 28 +++++++++- InvenTree/templates/js/translated/label.js | 50 ++++++++++++------ 4 files changed, 111 insertions(+), 28 deletions(-) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 99e51b48b4..0a06e76b9e 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from io import BytesIO +from PIL import Image + from django.utils.translation import ugettext_lazy as _ from django.conf import settings from django.conf.urls import url, include from django.core.exceptions import ValidationError, FieldError -from django.http import HttpResponse +from django.http import HttpResponse, JsonResponse from django_filters.rest_framework import DjangoFilterBackend @@ -14,6 +17,7 @@ from rest_framework import generics, filters from rest_framework.response import Response import InvenTree.helpers +from InvenTree.tasks import offload_task import common.models from plugin.registry import registry @@ -102,6 +106,8 @@ class LabelPrintMixin: label_name = "label.pdf" + label_names = [] + # Merge one or more PDF files into a single download for item in items_to_print: label = self.get_object() @@ -109,6 +115,8 @@ class LabelPrintMixin: label_name = label.generate_filename(request) + label_names.append(label_name) + if debug_mode: outputs.append(label.render_as_string(request)) else: @@ -117,7 +125,46 @@ class LabelPrintMixin: if not label_name.endswith(".pdf"): label_name += ".pdf" - if debug_mode: + if plugin is not None: + """ + Label printing is to be handled by a plugin, + rather than being exported to PDF. + + In this case, we do the following: + + - Individually generate each label, exporting as an image file + - Pass all the images through to the label printing plugin + - Return a JSON response indicating that the printing has been offloaded + + """ + + for output in outputs: + """ + For each output, we generate a temporary image file, + which will then get sent to the printer + """ + + # Generate a png image at 300dpi + (img_data, w, h) = output.get_document().write_png(resolution=300) + + # Construct a BytesIO object, which can be read by pillow + img_bytes = BytesIO(img_data) + + image = Image.open(img_bytes) + + # Offload a background task to print the provided label + offload_task( + 'plugin.events.print_label', + plugin.plugin_slug(), + image + ) + + return JsonResponse({ + 'plugin': plugin.plugin_slug(), + 'labels': label_names, + }) + + elif debug_mode: """ Contatenate all rendered templates into a single HTML string, and return the string as a HTML response. @@ -126,6 +173,7 @@ class LabelPrintMixin: html = "\n".join(outputs) return HttpResponse(html) + else: """ Concatenate all rendered pages into a single PDF object, diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 2d56e61929..2f0cfb02ce 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -415,15 +415,6 @@ class LabelPrintingMixin: def get_printer_name(self): return self.PRINTER_NAME - def print_labels(self, labels, **kwargs): - """ - Print multiple labels. - Default implementation is to call print_label() for each label, - but it can be overridden if desired. - """ - for label in labels: - self.print_label(label, **kwargs) - def print_label(self, label, **kwargs): """ Callback to print a single label diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index 049c8626c5..25a693a4f8 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -95,7 +95,11 @@ def process_event(plugin_slug, event, *args, **kwargs): logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'") - plugin = registry.plugins[plugin_slug] + plugin = registry.plugins.get(plugin_slug, None) + + if plugin is None: + logger.error(f"Could not find matching plugin for '{plugin_slug}'") + return plugin.process_event(event, *args, **kwargs) @@ -186,3 +190,25 @@ def after_delete(sender, instance, **kwargs): model=sender.__name__, table=table, ) + + +def print_label(plugin_slug, label_image, **kwargs): + """ + Print label with the provided plugin. + + This task is nominally handled by the background worker. + + Arguments: + plugin_slug: The unique slug (key) of the plugin + label_image: A PIL.Image image object to be printed + """ + + logger.info(f"Plugin '{plugin_slug}' is printing a label") + + plugin = registry.plugins.get(plugin_slug, None) + + if plugin is None: + logger.error(f"Could not find matching plugin for '{plugin_slug}'") + return + + plugin.print_label(label_image) diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js index f91d9c2431..93484d48d2 100644 --- a/InvenTree/templates/js/translated/label.js +++ b/InvenTree/templates/js/translated/label.js @@ -14,11 +14,41 @@ */ /* exported + printLabels, printPartLabels, printStockItemLabels, printStockLocationLabels, */ + +/* + * Perform the "print" action. + */ +function printLabels(url, plugin=null) { + + if (plugin) { + // If a plugin is provided, do not redirect the browser. + // Instead, perform an API request and display a message + + url = url + `plugin=${plugin}`; + + inventreeGet(url, {}, { + success: function(response) { + showMessage( + '{% trans "Labels sent to printer" %}', + { + style: 'success', + } + ); + } + }); + } else { + window.location.href = url; + } + +} + + function printStockItemLabels(items) { /** * Print stock item labels for the given stock items @@ -67,11 +97,7 @@ function printStockItemLabels(items) { href += `items[]=${item}&`; }); - if (data.plugin) { - href += `plugin=${data.plugin}`; - } - - window.location.href = href; + printLabels(href, data.plugin); } } ); @@ -80,6 +106,7 @@ function printStockItemLabels(items) { ); } + function printStockLocationLabels(locations) { if (locations.length == 0) { @@ -124,11 +151,7 @@ function printStockLocationLabels(locations) { href += `locations[]=${location}&`; }); - if (data.plugin) { - href += `plugin=${data.plugin}`; - } - - window.location.href = href; + printLabels(href, data.plugin); } } ); @@ -186,11 +209,7 @@ function printPartLabels(parts) { url += `parts[]=${part}&`; }); - if (data.plugin) { - href += `plugin=${data.plugin}`; - } - - window.location.href = url; + printLabels(href, data.plugin); } } ); @@ -234,7 +253,6 @@ function selectLabel(labels, items, options={}) { var plugin_selection = ''; - if (plugins.length > 0) { plugin_selection =`
From f50dab22a656dc289d695e5a9d7066959d671e74 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 16:17:04 +1100 Subject: [PATCH 17/89] fixes for label template --- InvenTree/label/templates/label/label_base.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/InvenTree/label/templates/label/label_base.html b/InvenTree/label/templates/label/label_base.html index 2986c8a439..363a7d3144 100644 --- a/InvenTree/label/templates/label/label_base.html +++ b/InvenTree/label/templates/label/label_base.html @@ -13,6 +13,8 @@ body { font-family: Arial, Helvetica, sans-serif; margin: 0mm; + color: #000; + background-color: #FFF; } img { From efc6af5f4cebcdf97646bedade27983128116a6f Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 16:18:08 +1100 Subject: [PATCH 18/89] PEP fixes --- InvenTree/label/api.py | 4 ++-- InvenTree/plugin/events.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 0a06e76b9e..1a679d5ac5 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -131,11 +131,11 @@ class LabelPrintMixin: rather than being exported to PDF. In this case, we do the following: - + - Individually generate each label, exporting as an image file - Pass all the images through to the label printing plugin - Return a JSON response indicating that the printing has been offloaded - + """ for output in outputs: diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index 25a693a4f8..829aeaa935 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -96,7 +96,7 @@ def process_event(plugin_slug, event, *args, **kwargs): logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'") plugin = registry.plugins.get(plugin_slug, None) - + if plugin is None: logger.error(f"Could not find matching plugin for '{plugin_slug}'") return @@ -206,7 +206,7 @@ def print_label(plugin_slug, label_image, **kwargs): logger.info(f"Plugin '{plugin_slug}' is printing a label") plugin = registry.plugins.get(plugin_slug, None) - + if plugin is None: logger.error(f"Could not find matching plugin for '{plugin_slug}'") return From fcdd1eb55b6528c977e2cdb90c518ae0d57a1d87 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 16:18:50 +1100 Subject: [PATCH 19/89] Remove old function call --- InvenTree/plugin/builtin/integration/mixins.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 2f0cfb02ce..451ddaf40f 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -412,9 +412,6 @@ class LabelPrintingMixin: super().__init__() self.add_mixin('labels', True, __class__) - def get_printer_name(self): - return self.PRINTER_NAME - def print_label(self, label, **kwargs): """ Callback to print a single label From 2fc522ad1569802d849af8206f830b6f76da4d9f Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 16:34:31 +1100 Subject: [PATCH 20/89] Hard code url - it is not always available --- InvenTree/templates/js/translated/label.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js index 93484d48d2..b54905f3e6 100644 --- a/InvenTree/templates/js/translated/label.js +++ b/InvenTree/templates/js/translated/label.js @@ -233,10 +233,8 @@ function selectLabel(labels, items, options={}) { // Request a list of available label printing plugins from the server inventreeGet( - '{% url "api-plugin-list" %}', - { - - }, + `/api/plugins/`, + {}, { async: false, success: function(response) { From f24aa37f8fafcb6d0b7dc1ca5af92d18a4740d03 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 16:34:47 +1100 Subject: [PATCH 21/89] Allow labels to be printed from the "incomplete build output" table --- InvenTree/build/templates/build/detail.html | 30 +++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/InvenTree/build/templates/build/detail.html b/InvenTree/build/templates/build/detail.html index 21bddfd6ca..0ed3c01f7e 100644 --- a/InvenTree/build/templates/build/detail.html +++ b/InvenTree/build/templates/build/detail.html @@ -258,6 +258,19 @@
+ + + + {% include "filter_list.html" with id='incompletebuilditems' %}
{% endif %} @@ -468,6 +481,23 @@ inventreeGet( ) }); + $('#incomplete-output-print-label').click(function() { + var outputs = $('#build-output-table').bootstrapTable('getSelections'); + + if (outputs.length == 0) { + outputs = $('#build-output-table').bootstrapTable('getData'); + } + + var stock_id_values = []; + + outputs.forEach(function(output) { + stock_id_values.push(output.pk); + }); + + printStockItemLabels(stock_id_values); + + }); + {% endif %} {% if build.active and build.has_untracked_bom_items %} From b2b9ceec46823b535fb9b2b4b58e4ab311a5c129 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 16:38:01 +1100 Subject: [PATCH 22/89] javascript fixes --- InvenTree/templates/js/translated/label.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js index b54905f3e6..bb662e0b67 100644 --- a/InvenTree/templates/js/translated/label.js +++ b/InvenTree/templates/js/translated/label.js @@ -203,10 +203,10 @@ function printPartLabels(parts) { var pk = data.label; - var url = `/api/label/part/${pk}/print/?`; + var href = `/api/label/part/${pk}/print/?`; parts.forEach(function(part) { - url += `parts[]=${part}&`; + href += `parts[]=${part}&`; }); printLabels(href, data.plugin); From 708ba566cbb34f18cbb7cd484113c94f3754e772 Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 24 Mar 2022 16:44:46 +1100 Subject: [PATCH 23/89] URL fix --- InvenTree/templates/js/translated/label.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js index bb662e0b67..5215ce9d28 100644 --- a/InvenTree/templates/js/translated/label.js +++ b/InvenTree/templates/js/translated/label.js @@ -233,7 +233,7 @@ function selectLabel(labels, items, options={}) { // Request a list of available label printing plugins from the server inventreeGet( - `/api/plugins/`, + `/api/plugin/`, {}, { async: false, From acd796f12796f4dc971c7193878d553ac4b275f3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Thu, 24 Mar 2022 20:11:17 +1100 Subject: [PATCH 24/89] Add 'plugins_enabled' information to the root API endpoint --- InvenTree/InvenTree/api.py | 2 ++ InvenTree/InvenTree/version.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/api.py b/InvenTree/InvenTree/api.py index 7c8f71ea9a..09ec02f158 100644 --- a/InvenTree/InvenTree/api.py +++ b/InvenTree/InvenTree/api.py @@ -6,6 +6,7 @@ Main JSON interface views from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ +from django.conf import settings from django.http import JsonResponse from django_filters.rest_framework import DjangoFilterBackend @@ -37,6 +38,7 @@ class InfoView(AjaxView): 'instance': inventreeInstanceName(), 'apiVersion': inventreeApiVersion(), 'worker_running': is_worker_running(), + 'plugins_enabled': settings.PLUGINS_ENABLED, } return JsonResponse(data) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index 6d4848f436..bb36b28ff8 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,14 @@ import common.models INVENTREE_SW_VERSION = "0.7.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 32 +INVENTREE_API_VERSION = 33 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v33 -> 2022-03-24 + - Adds "plugins_enabled" information to root API endpoint + v32 -> 2022-03-19 - Adds "parameters" detail to Part API endpoint (use ¶meters=true) - Adds ability to filter PartParameterTemplate API by Part instance From 63b5b956105df731889b51ba1c6d3e00afb865c6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 13:43:19 +1100 Subject: [PATCH 25/89] Ensure that kwargs are correctly passed through the settings chain Problem: validate_unique fails - Otherwise, the object is not correctly passed - Then, validate_unique fails --- InvenTree/common/models.py | 46 ++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 288e3451ad..7a938735fc 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -79,7 +79,7 @@ class BaseInvenTreeSetting(models.Model): self.key = str(self.key).upper() self.clean(**kwargs) - self.validate_unique() + self.validate_unique(**kwargs) super().save() @@ -253,13 +253,6 @@ class BaseInvenTreeSetting(models.Model): if user is not None: settings = settings.filter(user=user) - try: - setting = settings.filter(**cls.get_filters(key, **kwargs)).first() - except (ValueError, cls.DoesNotExist): - setting = None - except (IntegrityError, OperationalError): - setting = None - plugin = kwargs.pop('plugin', None) if plugin: @@ -270,6 +263,15 @@ class BaseInvenTreeSetting(models.Model): kwargs['plugin'] = plugin + settings = settings.filter(plugin=plugin) + + try: + setting = settings.filter(**cls.get_filters(key, **kwargs)).first() + except (ValueError, cls.DoesNotExist): + setting = None + except (IntegrityError, OperationalError): + setting = None + # Setting does not exist! (Try to create it) if not setting: @@ -287,7 +289,7 @@ class BaseInvenTreeSetting(models.Model): try: # Wrap this statement in "atomic", so it can be rolled back if it fails with transaction.atomic(): - setting.save() + setting.save(**kwargs) except (IntegrityError, OperationalError): # It might be the case that the database isn't created yet pass @@ -438,17 +440,37 @@ class BaseInvenTreeSetting(models.Model): validator(self.value) def validate_unique(self, exclude=None, **kwargs): - """ Ensure that the key:value pair is unique. + """ + Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' is unique, using a case-insensitive comparison. + + Note that sub-classes (UserSetting, PluginSetting) use other filters + to determine if the setting is 'unique' or not """ super().validate_unique(exclude) + filters = { + 'key': self.key, + } + + user = getattr(self, 'user', None) + plugin = getattr(self, 'plugin', None) + + if user is not None: + filters['user'] = user + + if plugin is not None: + filters['plugin'] = plugin + try: - setting = self.__class__.objects.exclude(id=self.id).filter(**self.get_filters(self.key, **kwargs)) + # Check if a duplicate setting already exists + setting = self.__class__.objects.filter(**filters).exclude(id=self.id) + if setting.exists(): raise ValidationError({'key': _('Key string must be unique')}) + except self.DoesNotExist: pass @@ -1312,7 +1334,7 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): def get_setting_object(cls, key, user): return super().get_setting_object(key, user=user) - def validate_unique(self, exclude=None): + def validate_unique(self, exclude=None, **kwargs): return super().validate_unique(exclude=exclude, user=self.user) @classmethod From 5ee9af7f0ebed1cb56ea488299824daf0db0afb8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 14:10:05 +1100 Subject: [PATCH 26/89] Remove old "get_filters" function - It was confusing! - Manually filter where required --- InvenTree/common/models.py | 49 +++++++++++++++++++++++--------------- InvenTree/plugin/models.py | 17 ------------- 2 files changed, 30 insertions(+), 36 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index 7a938735fc..c72e9f5adb 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -230,10 +230,6 @@ class BaseInvenTreeSetting(models.Model): return choices - @classmethod - def get_filters(cls, key, **kwargs): - return {'key__iexact': key} - @classmethod def get_setting_object(cls, key, **kwargs): """ @@ -247,26 +243,30 @@ class BaseInvenTreeSetting(models.Model): settings = cls.objects.all() + filters = { + 'key__iexact': key, + } + # Filter by user user = kwargs.get('user', None) if user is not None: - settings = settings.filter(user=user) + filters['user'] = user - plugin = kwargs.pop('plugin', None) + # Filter by plugin + plugin = kwargs.get('plugin', None) - if plugin: + if plugin is not None: from plugin import InvenTreePluginBase if issubclass(plugin.__class__, InvenTreePluginBase): plugin = plugin.plugin_config() + filters['plugin'] = plugin kwargs['plugin'] = plugin - settings = settings.filter(plugin=plugin) - try: - setting = settings.filter(**cls.get_filters(key, **kwargs)).first() + setting = settings.filter(**filters).first() except (ValueError, cls.DoesNotExist): setting = None except (IntegrityError, OperationalError): @@ -344,8 +344,26 @@ class BaseInvenTreeSetting(models.Model): if change_user is not None and not change_user.is_staff: return + filters = { + 'key__iexact': key, + } + + user = kwargs.get('user', None) + plugin = kwargs.get('plugin', None) + + if user is not None: + filters['user'] = user + + if plugin is not None: + from plugin import InvenTreePluginBase + + if issubclass(plugin.__class__, InvenTreePluginBase): + filters['plugin'] = plugin.plugin_config() + else: + filters['plugin'] = plugin + try: - setting = cls.objects.get(**cls.get_filters(key, **kwargs)) + setting = cls.objects.get(**filters) except cls.DoesNotExist: if create: @@ -452,7 +470,7 @@ class BaseInvenTreeSetting(models.Model): super().validate_unique(exclude) filters = { - 'key': self.key, + 'key__iexact': self.key, } user = getattr(self, 'user', None) @@ -1337,13 +1355,6 @@ class InvenTreeUserSetting(BaseInvenTreeSetting): def validate_unique(self, exclude=None, **kwargs): return super().validate_unique(exclude=exclude, user=self.user) - @classmethod - def get_filters(cls, key, **kwargs): - return { - 'key__iexact': key, - 'user__id': kwargs['user'].id - } - def to_native_value(self): """ Return the "pythonic" value, diff --git a/InvenTree/plugin/models.py b/InvenTree/plugin/models.py index e33d452e0a..44eeafd012 100644 --- a/InvenTree/plugin/models.py +++ b/InvenTree/plugin/models.py @@ -175,23 +175,6 @@ class PluginSetting(common.models.BaseInvenTreeSetting): return super().get_setting_definition(key, **kwargs) - @classmethod - def get_filters(cls, key, **kwargs): - """ - Override filters method to ensure settings are filtered by plugin id - """ - - filters = super().get_filters(key, **kwargs) - - plugin = kwargs.get('plugin', None) - - if plugin: - if issubclass(plugin.__class__, InvenTreePluginBase): - plugin = plugin.plugin_config() - filters['plugin'] = plugin - - return filters - plugin = models.ForeignKey( PluginConfig, related_name='settings', From bc48a9584407a238886cddc8e23013cf84191fa0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 14:13:02 +1100 Subject: [PATCH 27/89] PEP fixes --- InvenTree/common/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/models.py b/InvenTree/common/models.py index c72e9f5adb..ad144cd8db 100644 --- a/InvenTree/common/models.py +++ b/InvenTree/common/models.py @@ -353,7 +353,7 @@ class BaseInvenTreeSetting(models.Model): if user is not None: filters['user'] = user - + if plugin is not None: from plugin import InvenTreePluginBase @@ -462,7 +462,7 @@ class BaseInvenTreeSetting(models.Model): Ensure that the key:value pair is unique. In addition to the base validators, this ensures that the 'key' is unique, using a case-insensitive comparison. - + Note that sub-classes (UserSetting, PluginSetting) use other filters to determine if the setting is 'unique' or not """ From 011d97db3da22d1ba222749683c419613214df2a Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 14:20:25 +1100 Subject: [PATCH 28/89] Tweak display of label printing plugins --- InvenTree/templates/js/translated/label.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/templates/js/translated/label.js b/InvenTree/templates/js/translated/label.js index 5215ce9d28..c0f4c2f735 100644 --- a/InvenTree/templates/js/translated/label.js +++ b/InvenTree/templates/js/translated/label.js @@ -263,7 +263,7 @@ function selectLabel(labels, items, options={}) { `; plugins.forEach(function(plugin) { - plugin_selection += ``; + plugin_selection += ``; }); plugin_selection += ` From c1b408f8a326bdc8ca649039cb8f202c052671f8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 15:57:32 +1100 Subject: [PATCH 29/89] Send notification if printing fails If label printing fails (due to a plugin error) then the user is notified. --- InvenTree/label/api.py | 7 ++++++- InvenTree/part/tasks.py | 1 - InvenTree/plugin/events.py | 26 ++++++++++++++++++++++++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/InvenTree/label/api.py b/InvenTree/label/api.py index 1a679d5ac5..5103d99676 100644 --- a/InvenTree/label/api.py +++ b/InvenTree/label/api.py @@ -138,6 +138,9 @@ class LabelPrintMixin: """ + # Label instance + label_instance = self.get_object() + for output in outputs: """ For each output, we generate a temporary image file, @@ -156,7 +159,9 @@ class LabelPrintMixin: offload_task( 'plugin.events.print_label', plugin.plugin_slug(), - image + image, + label_instance=label_instance, + user=request.user, ) return JsonResponse({ diff --git a/InvenTree/part/tasks.py b/InvenTree/part/tasks.py index 9bc34f83df..b5e02e1128 100644 --- a/InvenTree/part/tasks.py +++ b/InvenTree/part/tasks.py @@ -5,7 +5,6 @@ import logging from django.utils.translation import ugettext_lazy as _ - import InvenTree.helpers import InvenTree.tasks import common.notifications diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index 829aeaa935..7a2e0d2da3 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -7,12 +7,15 @@ from __future__ import unicode_literals import logging +from django.utils.translation import ugettext_lazy as _ + from django.conf import settings from django.db import transaction from django.db.models.signals import post_save, post_delete from django.dispatch.dispatcher import receiver from common.models import InvenTreeSetting +import common.notifications from InvenTree.ready import canAppAccessDatabase from InvenTree.tasks import offload_task @@ -192,12 +195,14 @@ def after_delete(sender, instance, **kwargs): ) -def print_label(plugin_slug, label_image, **kwargs): +def print_label(plugin_slug, label_image, label_instance=None, user=None): """ Print label with the provided plugin. This task is nominally handled by the background worker. + If the printing fails (throws an exception) then the user is notified. + Arguments: plugin_slug: The unique slug (key) of the plugin label_image: A PIL.Image image object to be printed @@ -211,4 +216,21 @@ def print_label(plugin_slug, label_image, **kwargs): logger.error(f"Could not find matching plugin for '{plugin_slug}'") return - plugin.print_label(label_image) + try: + plugin.print_label(label_image) + except Exception as e: + # Plugin threw an error - notify the user who attempted to print + + ctx = { + 'name': _('Label printing failed'), + 'message': str(e), + } + + logger.error(f"Label printing failed: Sending notification to user '{user}'") + + common.notifications.trigger_notifaction( + label_instance, + 'label.printing_failed', + targets=[user], + context=ctx, + ) From 9046c746282b9186be530e50776e0146116d1fc9 Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 16:05:35 +1100 Subject: [PATCH 30/89] Only send printing failed as a UI notification --- InvenTree/common/notifications.py | 12 ++++++++++-- InvenTree/plugin/events.py | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/InvenTree/common/notifications.py b/InvenTree/common/notifications.py index 9151f9b879..fe737fc919 100644 --- a/InvenTree/common/notifications.py +++ b/InvenTree/common/notifications.py @@ -154,11 +154,18 @@ class UIMessageNotification(SingleNotificationMethod): return True -def trigger_notifaction(obj, category=None, obj_ref='pk', targets=None, target_fnc=None, target_args=[], target_kwargs={}, context={}): +def trigger_notifaction(obj, category=None, obj_ref='pk', **kwargs): """ Send out a notification """ + targets = kwargs.get('targets', None) + target_fnc = kwargs.get('target_fnc', None) + target_args = kwargs.get('target_args', []) + target_kwargs = kwargs.get('target_kwargs', {}) + context = kwargs.get('context', {}) + delivery_methods = kwargs.get('delivery_methods', None) + # Check if data is importing currently if isImportingData(): return @@ -190,7 +197,8 @@ def trigger_notifaction(obj, category=None, obj_ref='pk', targets=None, target_f logger.info(f"Sending notification '{category}' for '{str(obj)}'") # Collect possible methods - delivery_methods = inheritors(NotificationMethod) + if delivery_methods is None: + delivery_methods = inheritors(NotificationMethod) for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]: logger.info(f"Triggering method '{method.METHOD_NAME}'") diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index 7a2e0d2da3..ac0eaf3914 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -233,4 +233,5 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None): 'label.printing_failed', targets=[user], context=ctx, + delivery_methods=[common.notifications.UIMessageNotification] ) From 052d9770ceb1a31f98f45876b1701584b693932c Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 16:39:43 +1100 Subject: [PATCH 31/89] Override view permissions for plugin list API - It is necessary for *any* logged in user to view this endpoint - This is how the user determines which plugins are available (e.g. for label printing!) --- InvenTree/plugin/api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index 9ab3b96724..15eefff286 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -9,6 +9,7 @@ from django.conf.urls import url, include from rest_framework import generics from rest_framework import status +from rest_framework import permissions from rest_framework.response import Response from common.api import GlobalSettingsPermissions @@ -22,6 +23,11 @@ class PluginList(generics.ListAPIView): - GET: Return a list of all PluginConfig objects """ + # Allow any logged in user to read this endpoint + # This is necessary to allow certain functionality, + # e.g. determining which label printing plugins are available + permission_classes = [permissions.IsAuthenticated] + serializer_class = PluginSerializers.PluginConfigSerializer queryset = PluginConfig.objects.all() From 15a221509b21b92866313f8eb18b83c16050d14d Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 16:40:30 +1100 Subject: [PATCH 32/89] Bump API version --- InvenTree/InvenTree/version.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/InvenTree/InvenTree/version.py b/InvenTree/InvenTree/version.py index bb36b28ff8..f97f134e4a 100644 --- a/InvenTree/InvenTree/version.py +++ b/InvenTree/InvenTree/version.py @@ -12,11 +12,14 @@ import common.models INVENTREE_SW_VERSION = "0.7.0 dev" # InvenTree API version -INVENTREE_API_VERSION = 33 +INVENTREE_API_VERSION = 34 """ Increment this API version number whenever there is a significant change to the API that any clients need to know about +v34 -> 2022-03-25 + - Change permissions for "plugin list" API endpoint (now allows any authenticated user) + v33 -> 2022-03-24 - Adds "plugins_enabled" information to root API endpoint From 6e30ec560334504c8afb5ca55198013bc152465a Mon Sep 17 00:00:00 2001 From: Oliver Date: Fri, 25 Mar 2022 16:42:20 +1100 Subject: [PATCH 33/89] PEP fix --- InvenTree/plugin/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/InvenTree/plugin/api.py b/InvenTree/plugin/api.py index 15eefff286..5a4ea7bae3 100644 --- a/InvenTree/plugin/api.py +++ b/InvenTree/plugin/api.py @@ -25,7 +25,7 @@ class PluginList(generics.ListAPIView): # Allow any logged in user to read this endpoint # This is necessary to allow certain functionality, - # e.g. determining which label printing plugins are available + # e.g. determining which label printing plugins are available permission_classes = [permissions.IsAuthenticated] serializer_class = PluginSerializers.PluginConfigSerializer From 03eb5441c72be8af1eb6e290af88201506ff7018 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 25 Mar 2022 21:43:10 +1100 Subject: [PATCH 34/89] Pin setuptools version - Recent update seems to have borked it --- docker/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/requirements.txt b/docker/requirements.txt index cd27bdb5ce..4370522dbb 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -1,7 +1,7 @@ # Base python requirements for docker containers # Basic package requirements -setuptools>=57.4.0 +setuptools>=57.4.0<=60.1.0 wheel>=0.37.0 invoke>=1.4.0 # Invoke build tool From fcfdec0ad63870794bca990d93142418f3baf6c1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 25 Mar 2022 21:47:38 +1100 Subject: [PATCH 35/89] Fix requirements.txt formatting --- docker/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/requirements.txt b/docker/requirements.txt index 4370522dbb..192d6dac6f 100644 --- a/docker/requirements.txt +++ b/docker/requirements.txt @@ -1,7 +1,7 @@ # Base python requirements for docker containers # Basic package requirements -setuptools>=57.4.0<=60.1.0 +setuptools>=57.4.0,<=60.1.0 wheel>=0.37.0 invoke>=1.4.0 # Invoke build tool From 3e484317d2d781dcf15a300fa34ce8843c9d10a3 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Fri, 25 Mar 2022 23:49:15 +1100 Subject: [PATCH 36/89] Register notification against the plugin class, not the particular label --- InvenTree/plugin/events.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index ac0eaf3914..cc0e7539b9 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -228,8 +228,9 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None): logger.error(f"Label printing failed: Sending notification to user '{user}'") + # Throw an error against the plugin instance common.notifications.trigger_notifaction( - label_instance, + plugin.plugin_config(), 'label.printing_failed', targets=[user], context=ctx, From e18493a3974b8fd23766963bbd3b0cea22783569 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 26 Mar 2022 13:43:48 +1100 Subject: [PATCH 37/89] Supply label width and height to the label printing plugin --- InvenTree/plugin/builtin/integration/mixins.py | 4 ++++ InvenTree/plugin/events.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/InvenTree/plugin/builtin/integration/mixins.py b/InvenTree/plugin/builtin/integration/mixins.py index 451ddaf40f..118f0b775b 100644 --- a/InvenTree/plugin/builtin/integration/mixins.py +++ b/InvenTree/plugin/builtin/integration/mixins.py @@ -419,6 +419,10 @@ class LabelPrintingMixin: Arguments: label: A black-and-white pillow Image object + kwargs: + length: The length of the label (in mm) + width: The width of the label (in mm) + """ # Unimplemented (to be implemented by the particular plugin class) diff --git a/InvenTree/plugin/events.py b/InvenTree/plugin/events.py index cc0e7539b9..b510b42683 100644 --- a/InvenTree/plugin/events.py +++ b/InvenTree/plugin/events.py @@ -217,7 +217,7 @@ def print_label(plugin_slug, label_image, label_instance=None, user=None): return try: - plugin.print_label(label_image) + plugin.print_label(label_image, width=label_instance.width, height=label_instance.height) except Exception as e: # Plugin threw an error - notify the user who attempted to print From 5b949b6f60bb296884063cb48b84516e0882a1a7 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Mar 2022 07:45:45 +1100 Subject: [PATCH 38/89] Fix sorting for location column in part table --- InvenTree/templates/js/translated/stock.js | 166 +-------------------- 1 file changed, 1 insertion(+), 165 deletions(-) diff --git a/InvenTree/templates/js/translated/stock.js b/InvenTree/templates/js/translated/stock.js index b7f4162621..ade8bc5a0a 100644 --- a/InvenTree/templates/js/translated/stock.js +++ b/InvenTree/templates/js/translated/stock.js @@ -1770,6 +1770,7 @@ function loadStockTable(table, options) { col = { field: 'location_detail.pathstring', title: '{% trans "Location" %}', + sortName: 'location', formatter: function(value, row) { return locationDetail(row); } @@ -1912,172 +1913,8 @@ function loadStockTable(table, options) { original: original, showColumns: true, columns: columns, - {% if False %} - groupByField: options.groupByField || 'part', - groupBy: grouping, - groupByFormatter: function(field, id, data) { - - var row = data[0]; - - if (field == 'part_detail.full_name') { - - var html = imageHoverIcon(row.part_detail.thumbnail); - - html += row.part_detail.full_name; - html += ` (${data.length} {% trans "items" %})`; - - html += makePartIcons(row.part_detail); - - return html; - } else if (field == 'part_detail.IPN') { - var ipn = row.part_detail.IPN; - - if (ipn) { - return ipn; - } else { - return '-'; - } - } else if (field == 'part_detail.description') { - return row.part_detail.description; - } else if (field == 'packaging') { - var packaging = []; - - data.forEach(function(item) { - var pkg = item.packaging; - - if (!pkg) { - pkg = '-'; - } - - if (!packaging.includes(pkg)) { - packaging.push(pkg); - } - }); - - if (packaging.length > 1) { - return "..."; - } else if (packaging.length == 1) { - return packaging[0]; - } else { - return "-"; - } - } else if (field == 'quantity') { - var stock = 0; - var items = 0; - - data.forEach(function(item) { - stock += parseFloat(item.quantity); - items += 1; - }); - - stock = +stock.toFixed(5); - - return `${stock} (${items} {% trans "items" %})`; - } else if (field == 'status') { - var statii = []; - - data.forEach(function(item) { - var status = String(item.status); - - if (!status || status == '') { - status = '-'; - } - - if (!statii.includes(status)) { - statii.push(status); - } - }); - - // Multiple status codes - if (statii.length > 1) { - return "..."; - } else if (statii.length == 1) { - return stockStatusDisplay(statii[0]); - } else { - return "-"; - } - } else if (field == 'batch') { - var batches = []; - - data.forEach(function(item) { - var batch = item.batch; - - if (!batch || batch == '') { - batch = '-'; - } - - if (!batches.includes(batch)) { - batches.push(batch); - } - }); - - if (batches.length > 1) { - return "" + batches.length + " {% trans 'batches' %}"; - } else if (batches.length == 1) { - if (batches[0]) { - return batches[0]; - } else { - return '-'; - } - } else { - return '-'; - } - } else if (field == 'location_detail.pathstring') { - /* Determine how many locations */ - var locations = []; - - data.forEach(function(item) { - - var detail = locationDetail(item); - - if (!locations.includes(detail)) { - locations.push(detail); - } - }); - - if (locations.length == 1) { - // Single location, easy! - return locations[0]; - } else if (locations.length > 1) { - return "In " + locations.length + " {% trans 'locations' %}"; - } else { - return "{% trans 'Undefined location' %}"; - } - } else if (field == 'notes') { - var notes = []; - - data.forEach(function(item) { - var note = item.notes; - - if (!note || note == '') { - note = '-'; - } - - if (!notes.includes(note)) { - notes.push(note); - } - }); - - if (notes.length > 1) { - return '...'; - } else if (notes.length == 1) { - return notes[0] || '-'; - } else { - return '-'; - } - } else { - return ''; - } - }, - {% endif %} }); - /* - if (options.buttons) { - linkButtonsToSelection(table, options.buttons); - } - */ - var buttons = [ '#stock-print-options', '#stock-options', @@ -2092,7 +1929,6 @@ function loadStockTable(table, options) { buttons, ); - function stockAdjustment(action) { var items = $(table).bootstrapTable('getSelections'); From ce323d80ea4aadd1bbf56712450eccaae3e5e812 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Mar 2022 20:29:59 +1100 Subject: [PATCH 39/89] Fix floating point issues --- InvenTree/templates/js/translated/build.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/InvenTree/templates/js/translated/build.js b/InvenTree/templates/js/translated/build.js index 46f7f32e42..fd88144f8b 100644 --- a/InvenTree/templates/js/translated/build.js +++ b/InvenTree/templates/js/translated/build.js @@ -1025,9 +1025,10 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { } // Store the required quantity in the row data - row.required = quantity; + // Prevent weird rounding issues + row.required = parseFloat(quantity.toFixed(15)); - return quantity; + return row.required; } function sumAllocations(row) { @@ -1043,9 +1044,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) { quantity += item.quantity; }); - row.allocated = quantity; + row.allocated = parseFloat(quantity.toFixed(15)); - return quantity; + return row.allocated; } function setupCallbacks() { From ab7eda9d2c03cb96ac44d434c0d0bd640d40150c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 27 Mar 2022 21:04:14 +1100 Subject: [PATCH 40/89] Add new button to navbar --- InvenTree/templates/navbar.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/InvenTree/templates/navbar.html b/InvenTree/templates/navbar.html index 126376a7dc..898171a552 100644 --- a/InvenTree/templates/navbar.html +++ b/InvenTree/templates/navbar.html @@ -89,9 +89,16 @@ {% endif %} {% include "search_form.html" %}
- \ No newline at end of file + + + From ce396759a1a0aefafe923084647fa9d22ead6c15 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Mar 2022 10:09:09 +1100 Subject: [PATCH 43/89] Add new javascript file (search.js) - Empty, for now --- InvenTree/InvenTree/urls.py | 1 + InvenTree/templates/base.html | 1 + InvenTree/templates/js/translated/search.js | 8 ++++++++ 3 files changed, 10 insertions(+) create mode 100644 InvenTree/templates/js/translated/search.js diff --git a/InvenTree/InvenTree/urls.py b/InvenTree/InvenTree/urls.py index d795b81472..ec8b891f93 100644 --- a/InvenTree/InvenTree/urls.py +++ b/InvenTree/InvenTree/urls.py @@ -130,6 +130,7 @@ translated_javascript_urls = [ url(r'^order.js', DynamicJsView.as_view(template_name='js/translated/order.js'), name='order.js'), url(r'^part.js', DynamicJsView.as_view(template_name='js/translated/part.js'), name='part.js'), url(r'^report.js', DynamicJsView.as_view(template_name='js/translated/report.js'), name='report.js'), + url(r'^search.js', DynamicJsView.as_view(template_name='js/translated/search.js'), name='search.js'), url(r'^stock.js', DynamicJsView.as_view(template_name='js/translated/stock.js'), name='stock.js'), url(r'^plugin.js', DynamicJsView.as_view(template_name='js/translated/plugin.js'), name='plugin.js'), url(r'^tables.js', DynamicJsView.as_view(template_name='js/translated/tables.js'), name='tables.js'), diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index f916344bf9..4695984a9d 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -185,6 +185,7 @@ + diff --git a/InvenTree/templates/js/translated/search.js b/InvenTree/templates/js/translated/search.js new file mode 100644 index 0000000000..35a26963c3 --- /dev/null +++ b/InvenTree/templates/js/translated/search.js @@ -0,0 +1,8 @@ +{% load i18n %} + +/* globals +*/ + +/* exported +*/ + From ffb89af9e521dac143fadbecad4dd3c1d7ea4ad5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Mar 2022 10:10:02 +1100 Subject: [PATCH 44/89] Move search results panel into new template --- InvenTree/templates/base.html | 2 ++ InvenTree/templates/notifications.html | 16 ---------------- InvenTree/templates/search.html | 21 +++++++++++++++++++++ 3 files changed, 23 insertions(+), 16 deletions(-) create mode 100644 InvenTree/templates/search.html diff --git a/InvenTree/templates/base.html b/InvenTree/templates/base.html index 4695984a9d..6f364ce64b 100644 --- a/InvenTree/templates/base.html +++ b/InvenTree/templates/base.html @@ -126,9 +126,11 @@ {% endblock %} + {% include 'modals.html' %} {% include 'about.html' %} {% include "notifications.html" %} + {% include "search.html" %} diff --git a/InvenTree/templates/notifications.html b/InvenTree/templates/notifications.html index 5da84ef7b2..e7dfc86998 100644 --- a/InvenTree/templates/notifications.html +++ b/InvenTree/templates/notifications.html @@ -14,19 +14,3 @@ - diff --git a/InvenTree/templates/search.html b/InvenTree/templates/search.html new file mode 100644 index 0000000000..be6c9ca773 --- /dev/null +++ b/InvenTree/templates/search.html @@ -0,0 +1,21 @@ +{% load i18n %} + + + \ No newline at end of file From 7d68b3209b5499fa9538e0f412bace5674a26026 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Mon, 28 Mar 2022 10:18:49 +1100 Subject: [PATCH 45/89] Add callbacks for search panel opening and closing --- .../static/script/inventree/inventree.js | 5 ++++ InvenTree/templates/js/translated/search.js | 26 +++++++++++++++++++ InvenTree/templates/search.html | 4 +-- 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/InvenTree/InvenTree/static/script/inventree/inventree.js b/InvenTree/InvenTree/static/script/inventree/inventree.js index c71231ec87..b7cacd5860 100644 --- a/InvenTree/InvenTree/static/script/inventree/inventree.js +++ b/InvenTree/InvenTree/static/script/inventree/inventree.js @@ -231,6 +231,11 @@ function inventreeDocReady() { stopNotificationWatcher(); }); + // Calbacks for search panel + $('#offcanvas-search').on('show.bs.offcanvas', openSearchPanel); + $('#offcanvas-search').on('hidden.bs.offcanvas', closeSearchPanel); + + // Callbacks for notifications panel $('#offcanvas-notification').on('show.bs.offcanvas', openNotificationPanel); // listener for opening the notification panel $('#offcanvas-notification').on('hidden.bs.offcanvas', closeNotificationPanel); // listener for closing the notification panel } diff --git a/InvenTree/templates/js/translated/search.js b/InvenTree/templates/js/translated/search.js index 35a26963c3..b9121a2fea 100644 --- a/InvenTree/templates/js/translated/search.js +++ b/InvenTree/templates/js/translated/search.js @@ -4,5 +4,31 @@ */ /* exported + closeSearchPanel, + openSearchPanel, */ + +function closeSearchPanel() { + + // TODO + +} + + +/* + * Callback when the search panel is opened. + * Ensure the panel is in a known state + */ +function openSearchPanel() { + + var panel = $('#offcanvas-search'); + + // Ensure the 'no results found' element is visible + panel.find('#search-no-results').show(); + + // Finally, grab keyboard focus in the search bar + panel.find('#search-input').focus(); + +} + diff --git a/InvenTree/templates/search.html b/InvenTree/templates/search.html index be6c9ca773..1780abbdee 100644 --- a/InvenTree/templates/search.html +++ b/InvenTree/templates/search.html @@ -3,7 +3,7 @@ From 78e5a685dc91e60c9bb50c19b3040e28655a5a35 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Tue, 29 Mar 2022 22:35:47 +1100 Subject: [PATCH 54/89] Display results for parts and stock items - Use the existing rendering functions --- InvenTree/InvenTree/static/css/inventree.css | 26 +++++++ .../js/translated/model_renderers.js | 36 ++++----- InvenTree/templates/js/translated/search.js | 75 +++++++++++++++---- InvenTree/templates/search.html | 15 +++- 4 files changed, 118 insertions(+), 34 deletions(-) diff --git a/InvenTree/InvenTree/static/css/inventree.css b/InvenTree/InvenTree/static/css/inventree.css index 3f6fffe19b..78f3f4addc 100644 --- a/InvenTree/InvenTree/static/css/inventree.css +++ b/InvenTree/InvenTree/static/css/inventree.css @@ -1018,4 +1018,30 @@ input[type='number']{ a { text-decoration: none; background-color: transparent; +} + +/* Quicksearch Panel */ + +.search-result-panel { + max-width: 800px; + width: 50% +} + +.search-result-group { + padding: 5px; + border: 1px solid var(--border-color); + margin-bottom: 10px; +} + +.search-result-group-buttons > button{ + padding: 2px; + padding-left: 5px; + padding-right: 5px; + font-size: 80%; +} + +.search-result-entry { + border-top: 1px solid var(--border-color); + padding: 3px; + margin-top: 3px; } \ No newline at end of file diff --git a/InvenTree/templates/js/translated/model_renderers.js b/InvenTree/templates/js/translated/model_renderers.js index e3abe1186f..5f81cb9abf 100644 --- a/InvenTree/templates/js/translated/model_renderers.js +++ b/InvenTree/templates/js/translated/model_renderers.js @@ -31,7 +31,7 @@ // Renderer for "Company" model // eslint-disable-next-line no-unused-vars -function renderCompany(name, data, parameters, options) { +function renderCompany(name, data, parameters={}, options={}) { var html = select2Thumbnail(data.image); @@ -45,7 +45,7 @@ function renderCompany(name, data, parameters, options) { // Renderer for "StockItem" model // eslint-disable-next-line no-unused-vars -function renderStockItem(name, data, parameters, options) { +function renderStockItem(name, data, parameters={}, options={}) { var image = blankImage(); @@ -111,7 +111,7 @@ function renderStockItem(name, data, parameters, options) { // Renderer for "StockLocation" model // eslint-disable-next-line no-unused-vars -function renderStockLocation(name, data, parameters, options) { +function renderStockLocation(name, data, parameters={}, options={}) { var level = '- '.repeat(data.level); @@ -133,7 +133,7 @@ function renderStockLocation(name, data, parameters, options) { } // eslint-disable-next-line no-unused-vars -function renderBuild(name, data, parameters, options) { +function renderBuild(name, data, parameters={}, options={}) { var image = null; @@ -154,7 +154,7 @@ function renderBuild(name, data, parameters, options) { // Renderer for "Part" model // eslint-disable-next-line no-unused-vars -function renderPart(name, data, parameters, options) { +function renderPart(name, data, parameters={}, options={}) { var html = select2Thumbnail(data.image); @@ -164,13 +164,14 @@ function renderPart(name, data, parameters, options) { html += ` - ${data.description}`; } - var extra = ''; + var stock_data = ''; - // Display available part quantity if (user_settings.PART_SHOW_QUANTITY_IN_FORMS) { - extra += partStockLabel(data); + stock_data = partStockLabel(data); } + var extra = ''; + if (!data.active) { extra += `{% trans "Inactive" %}`; } @@ -178,6 +179,7 @@ function renderPart(name, data, parameters, options) { html += ` + ${stock_data} ${extra} {% trans "Part ID" %}: ${data.pk} @@ -188,7 +190,7 @@ function renderPart(name, data, parameters, options) { // Renderer for "User" model // eslint-disable-next-line no-unused-vars -function renderUser(name, data, parameters, options) { +function renderUser(name, data, parameters={}, options={}) { var html = `${data.username}`; @@ -202,7 +204,7 @@ function renderUser(name, data, parameters, options) { // Renderer for "Owner" model // eslint-disable-next-line no-unused-vars -function renderOwner(name, data, parameters, options) { +function renderOwner(name, data, parameters={}, options={}) { var html = `${data.name}`; @@ -223,7 +225,7 @@ function renderOwner(name, data, parameters, options) { // Renderer for "PurchaseOrder" model // eslint-disable-next-line no-unused-vars -function renderPurchaseOrder(name, data, parameters, options) { +function renderPurchaseOrder(name, data, parameters={}, options={}) { var html = ''; var prefix = global_settings.PURCHASEORDER_REFERENCE_PREFIX; @@ -257,7 +259,7 @@ function renderPurchaseOrder(name, data, parameters, options) { // Renderer for "SalesOrder" model // eslint-disable-next-line no-unused-vars -function renderSalesOrder(name, data, parameters, options) { +function renderSalesOrder(name, data, parameters={}, options={}) { var html = `${data.reference}`; if (data.description) { @@ -277,7 +279,7 @@ function renderSalesOrder(name, data, parameters, options) { // Renderer for "SalesOrderShipment" model // eslint-disable-next-line no-unused-vars -function renderSalesOrderShipment(name, data, parameters, options) { +function renderSalesOrderShipment(name, data, parameters={}, options={}) { var so_prefix = global_settings.SALESORDER_REFERENCE_PREFIX; @@ -294,7 +296,7 @@ function renderSalesOrderShipment(name, data, parameters, options) { // Renderer for "PartCategory" model // eslint-disable-next-line no-unused-vars -function renderPartCategory(name, data, parameters, options) { +function renderPartCategory(name, data, parameters={}, options={}) { var level = '- '.repeat(data.level); @@ -310,7 +312,7 @@ function renderPartCategory(name, data, parameters, options) { } // eslint-disable-next-line no-unused-vars -function renderPartParameterTemplate(name, data, parameters, options) { +function renderPartParameterTemplate(name, data, parameters={}, options={}) { var units = ''; @@ -326,7 +328,7 @@ function renderPartParameterTemplate(name, data, parameters, options) { // Renderer for "ManufacturerPart" model // eslint-disable-next-line no-unused-vars -function renderManufacturerPart(name, data, parameters, options) { +function renderManufacturerPart(name, data, parameters={}, options={}) { var manufacturer_image = null; var part_image = null; @@ -355,7 +357,7 @@ function renderManufacturerPart(name, data, parameters, options) { // Renderer for "SupplierPart" model // eslint-disable-next-line no-unused-vars -function renderSupplierPart(name, data, parameters, options) { +function renderSupplierPart(name, data, parameters={}, options={}) { var supplier_image = null; var part_image = null; diff --git a/InvenTree/templates/js/translated/search.js b/InvenTree/templates/js/translated/search.js index 26ba8ed5dd..a3e4ea59de 100644 --- a/InvenTree/templates/js/translated/search.js +++ b/InvenTree/templates/js/translated/search.js @@ -32,6 +32,11 @@ function openSearchPanel() { panel.find('#search-input').on('keyup change', searchTextChanged); + // Callback for "clear search" button + panel.find('#search-clear').click(function() { + panel.find('#search-input').val(''); + clearSearchResults(); + }); } var searchRequests = []; @@ -80,7 +85,15 @@ function updateSearch() { }, { success: function(response) { - addSearchResults('part', response.results, '{% trans "Parts" %}'); + addSearchResults( + 'part', + response.results, + '{% trans "Parts" %}', + renderPart, + { + show_stock_data: false, + } + ); } } )); @@ -92,10 +105,19 @@ function updateSearch() { search: searchText, limit: 10, offset: 0, + part_detail: true, + location_detail: true, }, { success: function(response) { - addSearchResults('stock', response.results, '{% trans "Stock Items" %}'); + addSearchResults( + 'stock', + response.results, + '{% trans "Stock Items" %}', + renderStockItem, + { + } + ); } } )); @@ -115,7 +137,7 @@ function clearSearchResults() { // Add a group of results to the list -function addSearchResults(key, results, title, formatter) { +function addSearchResults(key, results, title, renderFunc, renderParams={}) { if (results.length == 0) { // Do not display this group, as there are no results @@ -126,21 +148,48 @@ function addSearchResults(key, results, title, formatter) { // Ensure the 'no results found' element is hidden panel.find('#search-no-results').hide(); - - var results_element = panel.find('#search-results'); - - var header = `search-results-${key}`; panel.find('#search-results').append(` -
-
${title}
+
+
+
${title}
+ +
+ + +
+
+
+
`); results.forEach(function(result) { - // results_html.append(formatter(result)); - var result_html = `
hello result
`; + var html = renderFunc(key, result, renderParams); - panel.find(`#${header}`).append(result_html); + var result_html = ` +
+ ${html} +
+ `; + + panel.find(`#search-result-list-${key}`).append(result_html); }); -} \ No newline at end of file + + // Expand results panel + panel.find(`#search-result-list-${key}`).toggle(); + + // Add callback for "toggle" button + panel.find(`#hide-results-${key}`).click(function() { + panel.find(`#search-result-list-${key}`).toggle(); + }); + + // Add callback for "remove" button + panel.find(`#remove-results-${key}`).click(function() { + panel.find(`#search-results-${key}`).remove(); + }); +} diff --git a/InvenTree/templates/search.html b/InvenTree/templates/search.html index d66df22961..4a3921b4a7 100644 --- a/InvenTree/templates/search.html +++ b/InvenTree/templates/search.html @@ -1,14 +1,21 @@ {% load i18n %} -