Merge remote-tracking branch 'inventree/master' into order-parts-wizard

This commit is contained in:
Oliver Walters 2022-03-24 20:54:38 +11:00
commit 293294cce8
12 changed files with 305 additions and 29 deletions

View File

@ -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)

View File

@ -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 &parameters=true)
- Adds ability to filter PartParameterTemplate API by Part instance

View File

@ -258,6 +258,19 @@
</a></li>
</ul>
</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' %}
</div>
{% 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 %}

View File

@ -1,10 +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
@ -12,8 +17,11 @@ 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
from stock.models import StockItem, StockLocation
from part.models import Part
@ -46,11 +54,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 = {
@ -66,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()
@ -73,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:
@ -81,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.
@ -90,6 +173,7 @@ class LabelPrintMixin:
html = "\n".join(outputs)
return HttpResponse(html)
else:
"""
Concatenate all rendered pages into a single PDF object,

View File

@ -13,6 +13,8 @@
body {
font-family: Arial, Helvetica, sans-serif;
margin: 0mm;
color: #000;
background-color: #FFF;
}
img {

View File

@ -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):
"""
@ -57,8 +61,19 @@ 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)
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)

View File

@ -393,6 +393,38 @@ class AppMixin:
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
"""
# Unimplemented (to be implemented by the particular plugin class)
...
class APICallMixin:
"""
Mixin that enables easier API calls for a plugin

View File

@ -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)

View File

@ -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',

View File

@ -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):

View File

@ -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}`;
}
},

View File

@ -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
@ -57,14 +87,17 @@ 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}&`;
});
window.location.href = href;
printLabels(href, data.plugin);
}
}
);
@ -73,6 +106,7 @@ function printStockItemLabels(items) {
);
}
function printStockLocationLabels(locations) {
if (locations.length == 0) {
@ -107,14 +141,17 @@ 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}&`;
});
window.location.href = href;
printLabels(href, data.plugin);
}
}
);
@ -162,14 +199,17 @@ function printPartLabels(parts) {
response,
parts,
{
success: function(pk) {
var url = `/api/label/part/${pk}/print/?`;
success: function(data) {
var pk = data.label;
var href = `/api/label/part/${pk}/print/?`;
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.
*/
// 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(
`/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.meta.description} - <small>${plugin.meta.human_name}</small></option>`;
});
plugin_selection += `
</select>
</div>
</div>
`;
}
var modal = options.modal || '#modal-form';
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'>
<div class='form-group'>
<label class='control-label requiredField' for='id_label'>
{% trans "Select Label" %}
{% trans "Select Label Template" %}
</label>
<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}
</select>
</div>
</div>
${plugin_selection}
</form>`;
openModal({
@ -255,14 +329,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,
});
}
});
}