mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge branch 'master' of https://github.com/inventree/InvenTree into matmair/issue2694
This commit is contained in:
commit
e1b3de001f
@ -6,6 +6,7 @@ Main JSON interface views
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.conf import settings
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
@ -37,6 +38,7 @@ class InfoView(AjaxView):
|
|||||||
'instance': inventreeInstanceName(),
|
'instance': inventreeInstanceName(),
|
||||||
'apiVersion': inventreeApiVersion(),
|
'apiVersion': inventreeApiVersion(),
|
||||||
'worker_running': is_worker_running(),
|
'worker_running': is_worker_running(),
|
||||||
|
'plugins_enabled': settings.PLUGINS_ENABLED,
|
||||||
}
|
}
|
||||||
|
|
||||||
return JsonResponse(data)
|
return JsonResponse(data)
|
||||||
|
@ -12,11 +12,17 @@ import common.models
|
|||||||
INVENTREE_SW_VERSION = "0.7.0 dev"
|
INVENTREE_SW_VERSION = "0.7.0 dev"
|
||||||
|
|
||||||
# InvenTree API version
|
# InvenTree API version
|
||||||
INVENTREE_API_VERSION = 32
|
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
|
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
|
||||||
|
|
||||||
v32 -> 2022-03-19
|
v32 -> 2022-03-19
|
||||||
- Adds "parameters" detail to Part API endpoint (use ¶meters=true)
|
- Adds "parameters" detail to Part API endpoint (use ¶meters=true)
|
||||||
- Adds ability to filter PartParameterTemplate API by Part instance
|
- Adds ability to filter PartParameterTemplate API by Part instance
|
||||||
|
@ -258,6 +258,19 @@
|
|||||||
</a></li>
|
</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Label Printing Actions -->
|
||||||
|
<div class='btn-group'>
|
||||||
|
<button id='output-print-options' class='btn btn-primary dropdown-toggle' type='button' data-bs-toggle='dropdown' title='{% trans "Printing Actions" %}'>
|
||||||
|
<span class='fas fa-print'></span> <span class='caret'></span>
|
||||||
|
</button>
|
||||||
|
<ul class='dropdown-menu'>
|
||||||
|
<li><a class='dropdown-item' href='#' id='incomplete-output-print-label' title='{% trans "Print labels" %}'>
|
||||||
|
<span class='fas fa-tags'></span> {% trans "Print labels" %}
|
||||||
|
</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% include "filter_list.html" with id='incompletebuilditems' %}
|
{% include "filter_list.html" with id='incompletebuilditems' %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if build.active and build.has_untracked_bom_items %}
|
{% if build.active and build.has_untracked_bom_items %}
|
||||||
|
@ -79,7 +79,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
self.key = str(self.key).upper()
|
self.key = str(self.key).upper()
|
||||||
|
|
||||||
self.clean(**kwargs)
|
self.clean(**kwargs)
|
||||||
self.validate_unique()
|
self.validate_unique(**kwargs)
|
||||||
|
|
||||||
super().save()
|
super().save()
|
||||||
|
|
||||||
@ -230,10 +230,6 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
return choices
|
return choices
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_filters(cls, key, **kwargs):
|
|
||||||
return {'key__iexact': key}
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_setting_object(cls, key, **kwargs):
|
def get_setting_object(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -247,29 +243,35 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
|
|
||||||
settings = cls.objects.all()
|
settings = cls.objects.all()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'key__iexact': key,
|
||||||
|
}
|
||||||
|
|
||||||
# Filter by user
|
# Filter by user
|
||||||
user = kwargs.get('user', None)
|
user = kwargs.get('user', None)
|
||||||
|
|
||||||
if user is not None:
|
if user is not None:
|
||||||
settings = settings.filter(user=user)
|
filters['user'] = user
|
||||||
|
|
||||||
try:
|
# Filter by plugin
|
||||||
setting = settings.filter(**cls.get_filters(key, **kwargs)).first()
|
plugin = kwargs.get('plugin', None)
|
||||||
except (ValueError, cls.DoesNotExist):
|
|
||||||
setting = None
|
|
||||||
except (IntegrityError, OperationalError):
|
|
||||||
setting = None
|
|
||||||
|
|
||||||
plugin = kwargs.pop('plugin', None)
|
if plugin is not None:
|
||||||
|
|
||||||
if plugin:
|
|
||||||
from plugin import InvenTreePluginBase
|
from plugin import InvenTreePluginBase
|
||||||
|
|
||||||
if issubclass(plugin.__class__, InvenTreePluginBase):
|
if issubclass(plugin.__class__, InvenTreePluginBase):
|
||||||
plugin = plugin.plugin_config()
|
plugin = plugin.plugin_config()
|
||||||
|
|
||||||
|
filters['plugin'] = plugin
|
||||||
kwargs['plugin'] = plugin
|
kwargs['plugin'] = plugin
|
||||||
|
|
||||||
|
try:
|
||||||
|
setting = settings.filter(**filters).first()
|
||||||
|
except (ValueError, cls.DoesNotExist):
|
||||||
|
setting = None
|
||||||
|
except (IntegrityError, OperationalError):
|
||||||
|
setting = None
|
||||||
|
|
||||||
# Setting does not exist! (Try to create it)
|
# Setting does not exist! (Try to create it)
|
||||||
if not setting:
|
if not setting:
|
||||||
|
|
||||||
@ -287,7 +289,7 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
try:
|
try:
|
||||||
# Wrap this statement in "atomic", so it can be rolled back if it fails
|
# Wrap this statement in "atomic", so it can be rolled back if it fails
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
setting.save()
|
setting.save(**kwargs)
|
||||||
except (IntegrityError, OperationalError):
|
except (IntegrityError, OperationalError):
|
||||||
# It might be the case that the database isn't created yet
|
# It might be the case that the database isn't created yet
|
||||||
pass
|
pass
|
||||||
@ -342,8 +344,26 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
if change_user is not None and not change_user.is_staff:
|
if change_user is not None and not change_user.is_staff:
|
||||||
return
|
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:
|
try:
|
||||||
setting = cls.objects.get(**cls.get_filters(key, **kwargs))
|
setting = cls.objects.get(**filters)
|
||||||
except cls.DoesNotExist:
|
except cls.DoesNotExist:
|
||||||
|
|
||||||
if create:
|
if create:
|
||||||
@ -438,17 +458,37 @@ class BaseInvenTreeSetting(models.Model):
|
|||||||
validator(self.value)
|
validator(self.value)
|
||||||
|
|
||||||
def validate_unique(self, exclude=None, **kwargs):
|
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'
|
In addition to the base validators, this ensures that the 'key'
|
||||||
is unique, using a case-insensitive comparison.
|
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)
|
super().validate_unique(exclude)
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'key__iexact': 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:
|
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():
|
if setting.exists():
|
||||||
raise ValidationError({'key': _('Key string must be unique')})
|
raise ValidationError({'key': _('Key string must be unique')})
|
||||||
|
|
||||||
except self.DoesNotExist:
|
except self.DoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -1200,6 +1240,13 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
'validator': bool,
|
'validator': bool,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'NOTIFICATION_SEND_EMAILS': {
|
||||||
|
'name': _('Enable email notifications'),
|
||||||
|
'description': _('Allow sending of emails for event notifications'),
|
||||||
|
'default': True,
|
||||||
|
'validator': bool,
|
||||||
|
},
|
||||||
|
|
||||||
"LABEL_INLINE": {
|
"LABEL_INLINE": {
|
||||||
'name': _('Inline label display'),
|
'name': _('Inline label display'),
|
||||||
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
|
'description': _('Display PDF labels in the browser, instead of downloading as a file'),
|
||||||
@ -1305,16 +1352,9 @@ class InvenTreeUserSetting(BaseInvenTreeSetting):
|
|||||||
def get_setting_object(cls, key, user):
|
def get_setting_object(cls, key, user):
|
||||||
return super().get_setting_object(key, user=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)
|
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):
|
def to_native_value(self):
|
||||||
"""
|
"""
|
||||||
Return the "pythonic" value,
|
Return the "pythonic" value,
|
||||||
|
@ -8,15 +8,19 @@ from allauth.account.models import EmailAddress
|
|||||||
from InvenTree.helpers import inheritors
|
from InvenTree.helpers import inheritors
|
||||||
from InvenTree.ready import isImportingData
|
from InvenTree.ready import isImportingData
|
||||||
from common.models import NotificationEntry, NotificationMessage
|
from common.models import NotificationEntry, NotificationMessage
|
||||||
|
from common.models import InvenTreeUserSetting
|
||||||
|
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger('inventree')
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
# region notification classes
|
|
||||||
# region base classes
|
|
||||||
class NotificationMethod:
|
class NotificationMethod:
|
||||||
|
"""
|
||||||
|
Base class for notification methods
|
||||||
|
"""
|
||||||
|
|
||||||
METHOD_NAME = ''
|
METHOD_NAME = ''
|
||||||
CONTEXT_BUILTIN = ['name', 'message', ]
|
CONTEXT_BUILTIN = ['name', 'message', ]
|
||||||
CONTEXT_EXTRA = []
|
CONTEXT_EXTRA = []
|
||||||
@ -95,10 +99,8 @@ class SingleNotificationMethod(NotificationMethod):
|
|||||||
class BulkNotificationMethod(NotificationMethod):
|
class BulkNotificationMethod(NotificationMethod):
|
||||||
def send_bulk(self):
|
def send_bulk(self):
|
||||||
raise NotImplementedError('The `send` method must be overriden!')
|
raise NotImplementedError('The `send` method must be overriden!')
|
||||||
# endregion
|
|
||||||
|
|
||||||
|
|
||||||
# region implementations
|
|
||||||
class EmailNotification(BulkNotificationMethod):
|
class EmailNotification(BulkNotificationMethod):
|
||||||
METHOD_NAME = 'mail'
|
METHOD_NAME = 'mail'
|
||||||
CONTEXT_EXTRA = [
|
CONTEXT_EXTRA = [
|
||||||
@ -108,13 +110,26 @@ class EmailNotification(BulkNotificationMethod):
|
|||||||
]
|
]
|
||||||
|
|
||||||
def get_targets(self):
|
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(
|
return EmailAddress.objects.filter(
|
||||||
user__in=self.targets,
|
user__in=allowed_users,
|
||||||
)
|
)
|
||||||
|
|
||||||
def send_bulk(self):
|
def send_bulk(self):
|
||||||
html_message = render_to_string(self.context['template']['html'], self.context)
|
html_message = render_to_string(self.context['template']['html'], self.context)
|
||||||
targets = self.targets.values_list('email', flat=True)
|
targets = self.get_targets().values_list('email', flat=True)
|
||||||
|
|
||||||
InvenTree.tasks.send_email(self.context['template']['subject'], '', targets, html_message=html_message)
|
InvenTree.tasks.send_email(self.context['template']['subject'], '', targets, html_message=html_message)
|
||||||
|
|
||||||
@ -137,20 +152,27 @@ class UIMessageNotification(SingleNotificationMethod):
|
|||||||
message=self.context['message'],
|
message=self.context['message'],
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
# endregion
|
|
||||||
# endregion
|
|
||||||
|
|
||||||
|
|
||||||
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 an notification
|
Send out a notification
|
||||||
"""
|
"""
|
||||||
# check if data is importet currently
|
|
||||||
|
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():
|
if isImportingData():
|
||||||
return
|
return
|
||||||
|
|
||||||
# Resolve objekt reference
|
# Resolve objekt reference
|
||||||
obj_ref_value = getattr(obj, obj_ref)
|
obj_ref_value = getattr(obj, obj_ref)
|
||||||
|
|
||||||
# Try with some defaults
|
# Try with some defaults
|
||||||
if not obj_ref_value:
|
if not obj_ref_value:
|
||||||
obj_ref_value = getattr(obj, 'pk')
|
obj_ref_value = getattr(obj, 'pk')
|
||||||
@ -175,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)}'")
|
logger.info(f"Sending notification '{category}' for '{str(obj)}'")
|
||||||
|
|
||||||
# Collect possible methods
|
# 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]]:
|
for method in [a for a in delivery_methods if a not in [SingleNotificationMethod, BulkNotificationMethod]]:
|
||||||
logger.info(f"Triggering method '{method.METHOD_NAME}'")
|
logger.info(f"Triggering method '{method.METHOD_NAME}'")
|
||||||
|
@ -1,10 +1,15 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.conf.urls import url, include
|
from django.conf.urls import url, include
|
||||||
from django.core.exceptions import ValidationError, FieldError
|
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
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
|
||||||
@ -12,8 +17,11 @@ from rest_framework import generics, filters
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
|
from InvenTree.tasks import offload_task
|
||||||
import common.models
|
import common.models
|
||||||
|
|
||||||
|
from plugin.registry import registry
|
||||||
|
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
from part.models import Part
|
from part.models import Part
|
||||||
|
|
||||||
@ -46,11 +54,43 @@ class LabelPrintMixin:
|
|||||||
Mixin for printing labels
|
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):
|
def print(self, request, items_to_print):
|
||||||
"""
|
"""
|
||||||
Print this label template against a number of pre-validated items
|
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:
|
if len(items_to_print) == 0:
|
||||||
# No valid items provided, return an error message
|
# No valid items provided, return an error message
|
||||||
data = {
|
data = {
|
||||||
@ -66,6 +106,8 @@ class LabelPrintMixin:
|
|||||||
|
|
||||||
label_name = "label.pdf"
|
label_name = "label.pdf"
|
||||||
|
|
||||||
|
label_names = []
|
||||||
|
|
||||||
# Merge one or more PDF files into a single download
|
# Merge one or more PDF files into a single download
|
||||||
for item in items_to_print:
|
for item in items_to_print:
|
||||||
label = self.get_object()
|
label = self.get_object()
|
||||||
@ -73,6 +115,8 @@ class LabelPrintMixin:
|
|||||||
|
|
||||||
label_name = label.generate_filename(request)
|
label_name = label.generate_filename(request)
|
||||||
|
|
||||||
|
label_names.append(label_name)
|
||||||
|
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
outputs.append(label.render_as_string(request))
|
outputs.append(label.render_as_string(request))
|
||||||
else:
|
else:
|
||||||
@ -81,7 +125,51 @@ class LabelPrintMixin:
|
|||||||
if not label_name.endswith(".pdf"):
|
if not label_name.endswith(".pdf"):
|
||||||
label_name += ".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
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Label instance
|
||||||
|
label_instance = self.get_object()
|
||||||
|
|
||||||
|
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,
|
||||||
|
label_instance=label_instance,
|
||||||
|
user=request.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'plugin': plugin.plugin_slug(),
|
||||||
|
'labels': label_names,
|
||||||
|
})
|
||||||
|
|
||||||
|
elif debug_mode:
|
||||||
"""
|
"""
|
||||||
Contatenate all rendered templates into a single HTML string,
|
Contatenate all rendered templates into a single HTML string,
|
||||||
and return the string as a HTML response.
|
and return the string as a HTML response.
|
||||||
@ -90,6 +178,7 @@ class LabelPrintMixin:
|
|||||||
html = "\n".join(outputs)
|
html = "\n".join(outputs)
|
||||||
|
|
||||||
return HttpResponse(html)
|
return HttpResponse(html)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
"""
|
"""
|
||||||
Concatenate all rendered pages into a single PDF object,
|
Concatenate all rendered pages into a single PDF object,
|
||||||
|
@ -13,6 +13,8 @@
|
|||||||
body {
|
body {
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
margin: 0mm;
|
margin: 0mm;
|
||||||
|
color: #000;
|
||||||
|
background-color: #FFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -5,7 +5,6 @@ import logging
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
import InvenTree.helpers
|
import InvenTree.helpers
|
||||||
import InvenTree.tasks
|
import InvenTree.tasks
|
||||||
import common.notifications
|
import common.notifications
|
||||||
|
@ -8,6 +8,7 @@ over and above the built-in Django tags.
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
|
|
||||||
@ -31,6 +32,9 @@ from plugin.models import PluginSetting
|
|||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger('inventree')
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag()
|
@register.simple_tag()
|
||||||
def define(value, *args, **kwargs):
|
def define(value, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -57,8 +61,19 @@ def render_date(context, date_object):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if type(date_object) == str:
|
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
|
# 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!
|
# We may have already pre-cached the date format by calling this already!
|
||||||
user_date_format = context.get('user_date_format', None)
|
user_date_format = context.get('user_date_format', None)
|
||||||
|
@ -9,6 +9,7 @@ from django.conf.urls import url, include
|
|||||||
|
|
||||||
from rest_framework import generics
|
from rest_framework import generics
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework import permissions
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from common.api import GlobalSettingsPermissions
|
from common.api import GlobalSettingsPermissions
|
||||||
@ -22,6 +23,11 @@ class PluginList(generics.ListAPIView):
|
|||||||
- GET: Return a list of all PluginConfig objects
|
- 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
|
serializer_class = PluginSerializers.PluginConfigSerializer
|
||||||
queryset = PluginConfig.objects.all()
|
queryset = PluginConfig.objects.all()
|
||||||
|
|
||||||
|
@ -393,6 +393,42 @@ class AppMixin:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class LabelPrintingMixin:
|
||||||
|
"""
|
||||||
|
Mixin which enables direct printing of stock labels.
|
||||||
|
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
Meta options for this mixin
|
||||||
|
"""
|
||||||
|
MIXIN_NAME = 'Label printing'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.add_mixin('labels', True, __class__)
|
||||||
|
|
||||||
|
def print_label(self, label, **kwargs):
|
||||||
|
"""
|
||||||
|
Callback to print a single label
|
||||||
|
|
||||||
|
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)
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
class APICallMixin:
|
class APICallMixin:
|
||||||
"""
|
"""
|
||||||
Mixin that enables easier API calls for a plugin
|
Mixin that enables easier API calls for a plugin
|
||||||
|
@ -7,12 +7,15 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models.signals import post_save, post_delete
|
from django.db.models.signals import post_save, post_delete
|
||||||
from django.dispatch.dispatcher import receiver
|
from django.dispatch.dispatcher import receiver
|
||||||
|
|
||||||
from common.models import InvenTreeSetting
|
from common.models import InvenTreeSetting
|
||||||
|
import common.notifications
|
||||||
|
|
||||||
from InvenTree.ready import canAppAccessDatabase
|
from InvenTree.ready import canAppAccessDatabase
|
||||||
from InvenTree.tasks import offload_task
|
from InvenTree.tasks import offload_task
|
||||||
@ -95,7 +98,11 @@ def process_event(plugin_slug, event, *args, **kwargs):
|
|||||||
|
|
||||||
logger.info(f"Plugin '{plugin_slug}' is processing triggered event '{event}'")
|
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)
|
plugin.process_event(event, *args, **kwargs)
|
||||||
|
|
||||||
@ -186,3 +193,46 @@ def after_delete(sender, instance, **kwargs):
|
|||||||
model=sender.__name__,
|
model=sender.__name__,
|
||||||
table=table,
|
table=table,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
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
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
'name': _('Label printing failed'),
|
||||||
|
'message': str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(f"Label printing failed: Sending notification to user '{user}'")
|
||||||
|
|
||||||
|
# Throw an error against the plugin instance
|
||||||
|
common.notifications.trigger_notifaction(
|
||||||
|
plugin.plugin_config(),
|
||||||
|
'label.printing_failed',
|
||||||
|
targets=[user],
|
||||||
|
context=ctx,
|
||||||
|
delivery_methods=[common.notifications.UIMessageNotification]
|
||||||
|
)
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
Utility class to enable simpler imports
|
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.action.mixins import ActionMixin
|
||||||
from ..builtin.barcode.mixins import BarcodeMixin
|
from ..builtin.barcode.mixins import BarcodeMixin
|
||||||
|
|
||||||
@ -10,6 +11,7 @@ __all__ = [
|
|||||||
'APICallMixin',
|
'APICallMixin',
|
||||||
'AppMixin',
|
'AppMixin',
|
||||||
'EventMixin',
|
'EventMixin',
|
||||||
|
'LabelPrintingMixin',
|
||||||
'NavigationMixin',
|
'NavigationMixin',
|
||||||
'ScheduleMixin',
|
'ScheduleMixin',
|
||||||
'SettingsMixin',
|
'SettingsMixin',
|
||||||
|
@ -175,23 +175,6 @@ class PluginSetting(common.models.BaseInvenTreeSetting):
|
|||||||
|
|
||||||
return super().get_setting_definition(key, **kwargs)
|
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(
|
plugin = models.ForeignKey(
|
||||||
PluginConfig,
|
PluginConfig,
|
||||||
related_name='settings',
|
related_name='settings',
|
||||||
|
@ -10,12 +10,13 @@ import pathlib
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from typing import OrderedDict
|
from typing import OrderedDict
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
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.conf.urls import url, include
|
||||||
from django.urls import clear_url_caches
|
from django.urls import clear_url_caches
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
@ -282,6 +283,8 @@ class PluginsRegistry:
|
|||||||
if not settings.PLUGIN_TESTING:
|
if not settings.PLUGIN_TESTING:
|
||||||
raise error # pragma: no cover
|
raise error # pragma: no cover
|
||||||
plugin_db_setting = None
|
plugin_db_setting = None
|
||||||
|
except (IntegrityError) as error:
|
||||||
|
logger.error(f"Error initializing plugin: {error}")
|
||||||
|
|
||||||
# Always activate if testing
|
# Always activate if testing
|
||||||
if settings.PLUGIN_TESTING or (plugin_db_setting and plugin_db_setting.active):
|
if settings.PLUGIN_TESTING or (plugin_db_setting and plugin_db_setting.active):
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
<div class='btn btn-secondary' type='button' id='mark-all' title='{% trans "Mark all as read" %}'>
|
<div class='btn btn-outline-secondary' type='button' id='mark-all' title='{% trans "Mark all as read" %}'>
|
||||||
<span class='fa fa-bookmark'></span> {% trans "Mark all as read" %}
|
<span class='fa fa-bookmark'></span> {% trans "Mark all as read" %}
|
||||||
</div>
|
</div>
|
||||||
<div class='btn btn-secondary' type='button' id='inbox-refresh' title='{% trans "Refresh Pending Notifications" %}'>
|
<div class='btn btn-secondary' type='button' id='inbox-refresh' title='{% trans "Refresh Pending Notifications" %}'>
|
||||||
|
@ -126,8 +126,12 @@ $("#mark-all").on('click', function() {
|
|||||||
{
|
{
|
||||||
read: false,
|
read: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
success: function(response) {
|
||||||
|
updateNotificationTables();
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
updateNotificationTables();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loadNotificationTable("#history-table", {
|
loadNotificationTable("#history-table", {
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
{% include "InvenTree/settings/user_settings.html" %}
|
{% include "InvenTree/settings/user_settings.html" %}
|
||||||
{% include "InvenTree/settings/user_homepage.html" %}
|
{% include "InvenTree/settings/user_homepage.html" %}
|
||||||
{% include "InvenTree/settings/user_search.html" %}
|
{% include "InvenTree/settings/user_search.html" %}
|
||||||
|
{% include "InvenTree/settings/user_notifications.html" %}
|
||||||
{% include "InvenTree/settings/user_labels.html" %}
|
{% include "InvenTree/settings/user_labels.html" %}
|
||||||
{% include "InvenTree/settings/user_reports.html" %}
|
{% include "InvenTree/settings/user_reports.html" %}
|
||||||
{% include "InvenTree/settings/user_display.html" %}
|
{% include "InvenTree/settings/user_display.html" %}
|
||||||
|
@ -14,6 +14,8 @@
|
|||||||
{% include "sidebar_item.html" with label='user-home' text=text icon="fa-home" %}
|
{% include "sidebar_item.html" with label='user-home' text=text icon="fa-home" %}
|
||||||
{% trans "Search Settings" as text %}
|
{% trans "Search Settings" as text %}
|
||||||
{% include "sidebar_item.html" with label='user-search' text=text icon="fa-search" %}
|
{% 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 %}
|
{% trans "Label Printing" as text %}
|
||||||
{% include "sidebar_item.html" with label='user-labels' text=text icon="fa-tag" %}
|
{% include "sidebar_item.html" with label='user-labels' text=text icon="fa-tag" %}
|
||||||
{% trans "Reporting" as text %}
|
{% trans "Reporting" as text %}
|
||||||
|
@ -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 %}
|
||||||
|
|
||||||
|
<div class='row'>
|
||||||
|
<table class='table table-striped table-condensed'>
|
||||||
|
<tbody>
|
||||||
|
{% include "InvenTree/settings/setting.html" with key="NOTIFICATION_SEND_EMAILS" icon='fa-envelope' user_setting=True %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock content %}
|
@ -213,7 +213,7 @@ function createBuildOutput(build_id, options) {
|
|||||||
success: function(data) {
|
success: function(data) {
|
||||||
if (data.next) {
|
if (data.next) {
|
||||||
fields.serial_numbers.placeholder = `{% trans "Next available serial number" %}: ${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}`;
|
fields.serial_numbers.placeholder = `{% trans "Latest serial number" %}: ${data.latest}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1025,9 +1025,10 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the required quantity in the row data
|
// 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) {
|
function sumAllocations(row) {
|
||||||
@ -1043,9 +1044,9 @@ function loadBuildOutputAllocationTable(buildInfo, output, options={}) {
|
|||||||
quantity += item.quantity;
|
quantity += item.quantity;
|
||||||
});
|
});
|
||||||
|
|
||||||
row.allocated = quantity;
|
row.allocated = parseFloat(quantity.toFixed(15));
|
||||||
|
|
||||||
return quantity;
|
return row.allocated;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupCallbacks() {
|
function setupCallbacks() {
|
||||||
@ -1642,6 +1643,9 @@ function allocateStockToBuild(build_id, part_id, bom_items, options={}) {
|
|||||||
remaining = 0;
|
remaining = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure the quantity sent to the form field is correctly formatted
|
||||||
|
remaining = parseFloat(remaining.toFixed(15));
|
||||||
|
|
||||||
// We only care about entries which are not yet fully allocated
|
// We only care about entries which are not yet fully allocated
|
||||||
if (remaining > 0) {
|
if (remaining > 0) {
|
||||||
table_entries += renderBomItemRow(bom_item, remaining);
|
table_entries += renderBomItemRow(bom_item, remaining);
|
||||||
|
@ -14,11 +14,41 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/* exported
|
/* exported
|
||||||
|
printLabels,
|
||||||
printPartLabels,
|
printPartLabels,
|
||||||
printStockItemLabels,
|
printStockItemLabels,
|
||||||
printStockLocationLabels,
|
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) {
|
function printStockItemLabels(items) {
|
||||||
/**
|
/**
|
||||||
* Print stock item labels for the given stock items
|
* Print stock item labels for the given stock items
|
||||||
@ -57,14 +87,17 @@ function printStockItemLabels(items) {
|
|||||||
response,
|
response,
|
||||||
items,
|
items,
|
||||||
{
|
{
|
||||||
success: function(pk) {
|
success: function(data) {
|
||||||
|
|
||||||
|
var pk = data.label;
|
||||||
|
|
||||||
var href = `/api/label/stock/${pk}/print/?`;
|
var href = `/api/label/stock/${pk}/print/?`;
|
||||||
|
|
||||||
items.forEach(function(item) {
|
items.forEach(function(item) {
|
||||||
href += `items[]=${item}&`;
|
href += `items[]=${item}&`;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.location.href = href;
|
printLabels(href, data.plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -73,6 +106,7 @@ function printStockItemLabels(items) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function printStockLocationLabels(locations) {
|
function printStockLocationLabels(locations) {
|
||||||
|
|
||||||
if (locations.length == 0) {
|
if (locations.length == 0) {
|
||||||
@ -107,14 +141,17 @@ function printStockLocationLabels(locations) {
|
|||||||
response,
|
response,
|
||||||
locations,
|
locations,
|
||||||
{
|
{
|
||||||
success: function(pk) {
|
success: function(data) {
|
||||||
|
|
||||||
|
var pk = data.label;
|
||||||
|
|
||||||
var href = `/api/label/location/${pk}/print/?`;
|
var href = `/api/label/location/${pk}/print/?`;
|
||||||
|
|
||||||
locations.forEach(function(location) {
|
locations.forEach(function(location) {
|
||||||
href += `locations[]=${location}&`;
|
href += `locations[]=${location}&`;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.location.href = href;
|
printLabels(href, data.plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -162,14 +199,17 @@ function printPartLabels(parts) {
|
|||||||
response,
|
response,
|
||||||
parts,
|
parts,
|
||||||
{
|
{
|
||||||
success: function(pk) {
|
success: function(data) {
|
||||||
var url = `/api/label/part/${pk}/print/?`;
|
|
||||||
|
var pk = data.label;
|
||||||
|
|
||||||
|
var href = `/api/label/part/${pk}/print/?`;
|
||||||
|
|
||||||
parts.forEach(function(part) {
|
parts.forEach(function(part) {
|
||||||
url += `parts[]=${part}&`;
|
href += `parts[]=${part}&`;
|
||||||
});
|
});
|
||||||
|
|
||||||
window.location.href = url;
|
printLabels(href, data.plugin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -188,18 +228,51 @@ function selectLabel(labels, items, options={}) {
|
|||||||
* (via AJAX) from the server.
|
* (via AJAX) from the server.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// If only a single label template is provided,
|
// Array of available plugins for label printing
|
||||||
// just run with that!
|
var plugins = [];
|
||||||
|
|
||||||
if (labels.length == 1) {
|
// Request a list of available label printing plugins from the server
|
||||||
if (options.success) {
|
inventreeGet(
|
||||||
options.success(labels[0].pk);
|
`/api/plugin/`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
async: false,
|
||||||
|
success: function(response) {
|
||||||
|
response.forEach(function(plugin) {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return;
|
var plugin_selection = '';
|
||||||
|
|
||||||
|
if (plugins.length > 0) {
|
||||||
|
plugin_selection =`
|
||||||
|
<div class='form-group'>
|
||||||
|
<label class='control-label requiredField' for='id_plugin'>
|
||||||
|
{% trans "Select Printer" %}
|
||||||
|
</label>
|
||||||
|
<div class='controls'>
|
||||||
|
<select id='id_plugin' class='select form-control' name='plugin'>
|
||||||
|
<option value='' title='{% trans "Export to PDF" %}'>{% trans "Export to PDF" %}</option>
|
||||||
|
`;
|
||||||
|
|
||||||
|
plugins.forEach(function(plugin) {
|
||||||
|
plugin_selection += `<option value='${plugin.key}' title='${plugin.meta.human_name}'>${plugin.name} - <small>${plugin.meta.human_name}</small></option>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
plugin_selection += `
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var modal = options.modal || '#modal-form';
|
var modal = options.modal || '#modal-form';
|
||||||
|
|
||||||
var label_list = makeOptionsList(
|
var label_list = makeOptionsList(
|
||||||
@ -233,14 +306,15 @@ function selectLabel(labels, items, options={}) {
|
|||||||
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
<form method='post' action='' class='js-modal-form' enctype='multipart/form-data'>
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label class='control-label requiredField' for='id_label'>
|
<label class='control-label requiredField' for='id_label'>
|
||||||
{% trans "Select Label" %}
|
{% trans "Select Label Template" %}
|
||||||
</label>
|
</label>
|
||||||
<div class='controls'>
|
<div class='controls'>
|
||||||
<select id='id_label' class='select form-control name='label'>
|
<select id='id_label' class='select form-control' name='label'>
|
||||||
${label_list}
|
${label_list}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
${plugin_selection}
|
||||||
</form>`;
|
</form>`;
|
||||||
|
|
||||||
openModal({
|
openModal({
|
||||||
@ -255,14 +329,17 @@ function selectLabel(labels, items, options={}) {
|
|||||||
|
|
||||||
modalSubmit(modal, function() {
|
modalSubmit(modal, function() {
|
||||||
|
|
||||||
var label = $(modal).find('#id_label');
|
var label = $(modal).find('#id_label').val();
|
||||||
|
var plugin = $(modal).find('#id_plugin').val();
|
||||||
var pk = label.val();
|
|
||||||
|
|
||||||
closeModal(modal);
|
closeModal(modal);
|
||||||
|
|
||||||
if (options.success) {
|
if (options.success) {
|
||||||
options.success(pk);
|
options.success({
|
||||||
|
// Return the selected label template and plugin
|
||||||
|
label: label,
|
||||||
|
plugin: plugin,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -253,7 +253,7 @@ function openNotificationPanel() {
|
|||||||
{
|
{
|
||||||
success: function(response) {
|
success: function(response) {
|
||||||
if (response.length == 0) {
|
if (response.length == 0) {
|
||||||
html = `<p class='text-muted'>{% trans "No unread notifications" %}</p>`;
|
html = `<p class='text-muted'><em>{% trans "No unread notifications" %}</em><span class='fas fa-check-circle icon-green float-right'></span></p>`;
|
||||||
} else {
|
} else {
|
||||||
// build up items
|
// build up items
|
||||||
response.forEach(function(item, index) {
|
response.forEach(function(item, index) {
|
||||||
|
@ -1770,6 +1770,7 @@ function loadStockTable(table, options) {
|
|||||||
col = {
|
col = {
|
||||||
field: 'location_detail.pathstring',
|
field: 'location_detail.pathstring',
|
||||||
title: '{% trans "Location" %}',
|
title: '{% trans "Location" %}',
|
||||||
|
sortName: 'location',
|
||||||
formatter: function(value, row) {
|
formatter: function(value, row) {
|
||||||
return locationDetail(row);
|
return locationDetail(row);
|
||||||
}
|
}
|
||||||
@ -1912,172 +1913,8 @@ function loadStockTable(table, options) {
|
|||||||
original: original,
|
original: original,
|
||||||
showColumns: true,
|
showColumns: true,
|
||||||
columns: columns,
|
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 += ` <i>(${data.length} {% trans "items" %})</i>`;
|
|
||||||
|
|
||||||
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 "<i>{% trans 'Undefined location' %}</i>";
|
|
||||||
}
|
|
||||||
} 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 = [
|
var buttons = [
|
||||||
'#stock-print-options',
|
'#stock-print-options',
|
||||||
'#stock-options',
|
'#stock-options',
|
||||||
@ -2092,7 +1929,6 @@ function loadStockTable(table, options) {
|
|||||||
buttons,
|
buttons,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
function stockAdjustment(action) {
|
function stockAdjustment(action) {
|
||||||
var items = $(table).bootstrapTable('getSelections');
|
var items = $(table).bootstrapTable('getSelections');
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Base python requirements for docker containers
|
# Base python requirements for docker containers
|
||||||
|
|
||||||
# Basic package requirements
|
# Basic package requirements
|
||||||
setuptools>=57.4.0
|
setuptools>=57.4.0,<=60.1.0
|
||||||
wheel>=0.37.0
|
wheel>=0.37.0
|
||||||
invoke>=1.4.0 # Invoke build tool
|
invoke>=1.4.0 # Invoke build tool
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user