mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge remote-tracking branch 'inventree/master' into order-parts-wizard
This commit is contained in:
commit
293294cce8
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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 %}
|
||||
|
@ -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,
|
||||
|
@ -13,6 +13,8 @@
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
margin: 0mm;
|
||||
color: #000;
|
||||
background-color: #FFF;
|
||||
}
|
||||
|
||||
img {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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):
|
||||
|
@ -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}`;
|
||||
}
|
||||
},
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user