Merge pull request #1342 from SchrodingersGat/label-improvements

Label improvements
This commit is contained in:
Oliver 2021-02-22 19:45:11 +11:00 committed by GitHub
commit b8327a5531
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 504 additions and 214 deletions

View File

@ -204,7 +204,6 @@ INSTALLED_APPS = [
'crispy_forms', # Improved form rendering 'crispy_forms', # Improved form rendering
'import_export', # Import / export tables to file 'import_export', # Import / export tables to file
'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files 'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files
'qr_code', # Generate QR codes
'mptt', # Modified Preorder Tree Traversal 'mptt', # Modified Preorder Tree Traversal
'markdownx', # Markdown editing 'markdownx', # Markdown editing
'markdownify', # Markdown template rendering 'markdownify', # Markdown template rendering
@ -250,6 +249,7 @@ TEMPLATES = [
os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, 'templates'),
# Allow templates in the reporting directory to be accessed # Allow templates in the reporting directory to be accessed
os.path.join(MEDIA_ROOT, 'report'), os.path.join(MEDIA_ROOT, 'report'),
os.path.join(MEDIA_ROOT, 'label'),
], ],
'APP_DIRS': True, 'APP_DIRS': True,
'OPTIONS': { 'OPTIONS': {
@ -407,8 +407,6 @@ CACHES = {
} }
} }
QR_CODE_CACHE_ALIAS = 'qr-code'
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators

View File

@ -125,6 +125,7 @@
.qr-container { .qr-container {
width: 100%; width: 100%;
align-content: center; align-content: center;
object-fit: fill;
} }
.navbar-brand { .navbar-brand {

View File

@ -9,7 +9,6 @@ from django.conf.urls import url, include
from django.urls import path from django.urls import path
from django.contrib import admin from django.contrib import admin
from django.contrib.auth import views as auth_views from django.contrib.auth import views as auth_views
from qr_code import urls as qr_code_urls
from company.urls import company_urls from company.urls import company_urls
from company.urls import supplier_part_urls from company.urls import supplier_part_urls
@ -21,7 +20,7 @@ from stock.urls import stock_urls
from build.urls import build_urls from build.urls import build_urls
from order.urls import order_urls from order.urls import order_urls
from barcode.api import barcode_api_urls from barcodes.api import barcode_api_urls
from common.api import common_api_urls from common.api import common_api_urls
from part.api import part_api_urls, bom_api_urls from part.api import part_api_urls, bom_api_urls
from company.api import company_api_urls from company.api import company_api_urls
@ -141,8 +140,6 @@ urlpatterns = [
url(r'^admin/shell/', include('django_admin_shell.urls')), url(r'^admin/shell/', include('django_admin_shell.urls')),
url(r'^admin/', admin.site.urls, name='inventree-admin'), url(r'^admin/', admin.site.urls, name='inventree-admin'),
url(r'^qr_code/', include(qr_code_urls, namespace='qr_code')),
url(r'^index/', IndexView.as_view(), name='index'), url(r'^index/', IndexView.as_view(), name='index'),
url(r'^search/', SearchView.as_view(), name='search'), url(r'^search/', SearchView.as_view(), name='search'),
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'), url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),

View File

@ -12,7 +12,7 @@ from rest_framework.views import APIView
from stock.models import StockItem from stock.models import StockItem
from stock.serializers import StockItemSerializer from stock.serializers import StockItemSerializer
from barcode.barcode import load_barcode_plugins, hash_barcode from barcodes.barcode import load_barcode_plugins, hash_barcode
class BarcodeScan(APIView): class BarcodeScan(APIView):

View File

@ -5,7 +5,7 @@ import hashlib
import logging import logging
from InvenTree import plugins as InvenTreePlugins from InvenTree import plugins as InvenTreePlugins
from barcode import plugins as BarcodePlugins from barcodes import plugins as BarcodePlugins
from stock.models import StockItem from stock.models import StockItem
from stock.serializers import StockItemSerializer, LocationSerializer from stock.serializers import StockItemSerializer, LocationSerializer

View File

@ -4,7 +4,7 @@
DigiKey barcode decoding DigiKey barcode decoding
""" """
from barcode.barcode import BarcodePlugin from barcodes.barcode import BarcodePlugin
class DigikeyBarcodePlugin(BarcodePlugin): class DigikeyBarcodePlugin(BarcodePlugin):

View File

@ -13,7 +13,7 @@ references model objects actually exist in the database.
import json import json
from barcode.barcode import BarcodePlugin from barcodes.barcode import BarcodePlugin
from stock.models import StockItem, StockLocation from stock.models import StockItem, StockLocation
from part.models import Part from part.models import Part

View File

@ -1,11 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import sys
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
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_filters.rest_framework import DjangoFilterBackend from django_filters.rest_framework import DjangoFilterBackend
@ -13,6 +12,7 @@ from rest_framework import generics, filters
from rest_framework.response import Response from rest_framework.response import Response
import InvenTree.helpers import InvenTree.helpers
import common.models
from stock.models import StockItem, StockLocation from stock.models import StockItem, StockLocation
@ -40,6 +40,74 @@ class LabelListView(generics.ListAPIView):
] ]
class LabelPrintMixin:
"""
Mixin for printing labels
"""
def print(self, request, items_to_print):
"""
Print this label template against a number of pre-validated items
"""
if len(items_to_print) == 0:
# No valid items provided, return an error message
data = {
'error': _('No valid objects provided to template'),
}
return Response(data, status=400)
outputs = []
# In debug mode, generate single HTML output, rather than PDF
debug_mode = common.models.InvenTreeSetting.get_setting('REPORT_DEBUG_MODE')
# Merge one or more PDF files into a single download
for item in items_to_print:
label = self.get_object()
label.object_to_print = item
if debug_mode:
outputs.append(label.render_as_string(request))
else:
outputs.append(label.render(request))
if debug_mode:
"""
Contatenate all rendered templates into a single HTML string,
and return the string as a HTML response.
"""
html = "\n".join(outputs)
return HttpResponse(html)
else:
"""
Concatenate all rendered pages into a single PDF object,
and return the resulting document!
"""
pages = []
if len(outputs) > 1:
# If more than one output is generated, merge them into a single file
for output in outputs:
doc = output.get_document()
for page in doc.pages:
pages.append(page)
pdf = outputs[0].get_document().copy(pages).write_pdf()
else:
pdf = outputs[0].get_document().write_pdf()
return InvenTree.helpers.DownloadFile(
pdf,
'inventree_label.pdf',
content_type='application/pdf'
)
class StockItemLabelMixin: class StockItemLabelMixin:
""" """
Mixin for extracting stock items from query params Mixin for extracting stock items from query params
@ -158,7 +226,7 @@ class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = StockItemLabelSerializer serializer_class = StockItemLabelSerializer
class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin): class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin, LabelPrintMixin):
""" """
API endpoint for printing a StockItemLabel object API endpoint for printing a StockItemLabel object
""" """
@ -173,34 +241,7 @@ class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin):
items = self.get_items() items = self.get_items()
if len(items) == 0: return self.print(request, items)
# No valid items provided, return an error message
data = {
'error': _('Must provide valid StockItem(s)'),
}
return Response(data, status=400)
label = self.get_object()
try:
pdf = label.render(items)
except:
e = sys.exc_info()[1]
data = {
'error': _('Error during label rendering'),
'message': str(e),
}
return Response(data, status=400)
return InvenTree.helpers.DownloadFile(
pdf.getbuffer(),
'stock_item_label.pdf',
content_type='application/pdf'
)
class StockLocationLabelMixin: class StockLocationLabelMixin:
@ -320,7 +361,7 @@ class StockLocationLabelDetail(generics.RetrieveUpdateDestroyAPIView):
serializer_class = StockLocationLabelSerializer serializer_class = StockLocationLabelSerializer
class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin): class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin, LabelPrintMixin):
""" """
API endpoint for printing a StockLocationLabel object API endpoint for printing a StockLocationLabel object
""" """
@ -332,35 +373,7 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin)
locations = self.get_locations() locations = self.get_locations()
if len(locations) == 0: return self.print(request, locations)
# No valid locations provided - return an error message
return Response(
{
'error': _('Must provide valid StockLocation(s)'),
},
status=400,
)
label = self.get_object()
try:
pdf = label.render(locations)
except:
e = sys.exc_info()[1]
data = {
'error': _('Error during label rendering'),
'message': str(e),
}
return Response(data, status=400)
return InvenTree.helpers.DownloadFile(
pdf.getbuffer(),
'stock_location_label.pdf',
content_type='application/pdf'
)
label_api_urls = [ label_api_urls = [

View File

@ -1,6 +1,7 @@
import os import os
import shutil import shutil
import logging import logging
import hashlib
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings from django.conf import settings
@ -9,6 +10,20 @@ from django.conf import settings
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def hashFile(filename):
"""
Calculate the MD5 hash of a file
"""
md5 = hashlib.md5()
with open(filename, 'rb') as f:
data = f.read()
md5.update(data)
return md5.hexdigest()
class LabelConfig(AppConfig): class LabelConfig(AppConfig):
name = 'label' name = 'label'
@ -35,6 +50,7 @@ class LabelConfig(AppConfig):
src_dir = os.path.join( src_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.path.dirname(os.path.realpath(__file__)),
'templates', 'templates',
'label',
'stockitem', 'stockitem',
) )
@ -54,6 +70,8 @@ class LabelConfig(AppConfig):
'file': 'qr.html', 'file': 'qr.html',
'name': 'QR Code', 'name': 'QR Code',
'description': 'Simple QR code label', 'description': 'Simple QR code label',
'width': 24,
'height': 24,
}, },
] ]
@ -70,7 +88,21 @@ class LabelConfig(AppConfig):
src_file = os.path.join(src_dir, label['file']) src_file = os.path.join(src_dir, label['file'])
dst_file = os.path.join(settings.MEDIA_ROOT, filename) dst_file = os.path.join(settings.MEDIA_ROOT, filename)
if not os.path.exists(dst_file): to_copy = False
if os.path.exists(dst_file):
# File already exists - let's see if it is the "same",
# or if we need to overwrite it with a newer copy!
if not hashFile(dst_file) == hashFile(src_file):
logger.info(f"Hash differs for '{filename}'")
to_copy = True
else:
logger.info(f"Label template '{filename}' is not present")
to_copy = True
if to_copy:
logger.info(f"Copying label template '{dst_file}'") logger.info(f"Copying label template '{dst_file}'")
shutil.copyfile(src_file, dst_file) shutil.copyfile(src_file, dst_file)
@ -86,7 +118,9 @@ class LabelConfig(AppConfig):
description=label['description'], description=label['description'],
label=filename, label=filename,
filters='', filters='',
enabled=True enabled=True,
width=label['width'],
height=label['height'],
) )
except: except:
pass pass
@ -106,6 +140,7 @@ class LabelConfig(AppConfig):
src_dir = os.path.join( src_dir = os.path.join(
os.path.dirname(os.path.realpath(__file__)), os.path.dirname(os.path.realpath(__file__)),
'templates', 'templates',
'label',
'stocklocation', 'stocklocation',
) )
@ -125,11 +160,15 @@ class LabelConfig(AppConfig):
'file': 'qr.html', 'file': 'qr.html',
'name': 'QR Code', 'name': 'QR Code',
'description': 'Simple QR code label', 'description': 'Simple QR code label',
'width': 24,
'height': 24,
}, },
{ {
'file': 'qr_and_text.html', 'file': 'qr_and_text.html',
'name': 'QR and text', 'name': 'QR and text',
'description': 'Label with QR code and name of location', 'description': 'Label with QR code and name of location',
'width': 50,
'height': 24,
} }
] ]
@ -146,7 +185,21 @@ class LabelConfig(AppConfig):
src_file = os.path.join(src_dir, label['file']) src_file = os.path.join(src_dir, label['file'])
dst_file = os.path.join(settings.MEDIA_ROOT, filename) dst_file = os.path.join(settings.MEDIA_ROOT, filename)
if not os.path.exists(dst_file): to_copy = False
if os.path.exists(dst_file):
# File already exists - let's see if it is the "same",
# or if we need to overwrite it with a newer copy!
if not hashFile(dst_file) == hashFile(src_file):
logger.info(f"Hash differs for '{filename}'")
to_copy = True
else:
logger.info(f"Label template '{filename}' is not present")
to_copy = True
if to_copy:
logger.info(f"Copying label template '{dst_file}'") logger.info(f"Copying label template '{dst_file}'")
shutil.copyfile(src_file, dst_file) shutil.copyfile(src_file, dst_file)
@ -162,7 +215,9 @@ class LabelConfig(AppConfig):
description=label['description'], description=label['description'],
label=filename, label=filename,
filters='', filters='',
enabled=True enabled=True,
width=label['width'],
height=label['height'],
) )
except: except:
pass pass

View File

@ -0,0 +1,34 @@
# Generated by Django 3.0.7 on 2021-02-22 04:35
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('label', '0005_auto_20210113_2302'),
]
operations = [
migrations.AddField(
model_name='stockitemlabel',
name='height',
field=models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]'),
),
migrations.AddField(
model_name='stockitemlabel',
name='width',
field=models.FloatField(default=50, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'),
),
migrations.AddField(
model_name='stocklocationlabel',
name='height',
field=models.FloatField(default=20, help_text='Label height, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Height [mm]'),
),
migrations.AddField(
model_name='stocklocationlabel',
name='width',
field=models.FloatField(default=50, help_text='Label width, specified in mm', validators=[django.core.validators.MinValueValidator(2)], verbose_name='Width [mm]'),
),
]

View File

@ -5,21 +5,35 @@ Label printing models
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
import sys
import os import os
import io import logging
import datetime
from blabel import LabelWriter
from django.conf import settings
from django.db import models from django.db import models
from django.core.validators import FileExtensionValidator from django.core.validators import FileExtensionValidator, MinValueValidator
from django.core.exceptions import ValidationError, FieldError from django.core.exceptions import ValidationError, FieldError
from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from InvenTree.helpers import validateFilterString, normalize from InvenTree.helpers import validateFilterString, normalize
import common.models
import stock.models import stock.models
try:
from django_weasyprint import WeasyTemplateResponseMixin
except OSError as err:
print("OSError: {e}".format(e=err))
print("You may require some further system packages to be installed.")
sys.exit(1)
logger = logging.getLogger(__name__)
def rename_label(instance, filename): def rename_label(instance, filename):
""" Place the label file into the correct subdirectory """ """ Place the label file into the correct subdirectory """
@ -43,6 +57,21 @@ def validate_stock_location_filters(filters):
return filters return filters
class WeasyprintLabelMixin(WeasyTemplateResponseMixin):
"""
Class for rendering a label to a PDF
"""
pdf_filename = 'label.pdf'
pdf_attachment = True
def __init__(self, request, template, **kwargs):
self.request = request
self.template_name = template
self.pdf_filename = kwargs.get('filename', 'label.pdf')
class LabelTemplate(models.Model): class LabelTemplate(models.Model):
""" """
Base class for generic, filterable labels. Base class for generic, filterable labels.
@ -54,6 +83,9 @@ class LabelTemplate(models.Model):
# Each class of label files will be stored in a separate subdirectory # Each class of label files will be stored in a separate subdirectory
SUBDIR = "label" SUBDIR = "label"
# Object we will be printing against (will be filled out later)
object_to_print = None
@property @property
def template(self): def template(self):
return self.label.path return self.label.path
@ -92,38 +124,90 @@ class LabelTemplate(models.Model):
help_text=_('Label template is enabled'), help_text=_('Label template is enabled'),
) )
def get_record_data(self, items): width = models.FloatField(
default=50,
verbose_name=('Width [mm]'),
help_text=_('Label width, specified in mm'),
validators=[MinValueValidator(2)]
)
height = models.FloatField(
default=20,
verbose_name=_('Height [mm]'),
help_text=_('Label height, specified in mm'),
validators=[MinValueValidator(2)]
)
@property
def template_name(self):
""" """
Return a list of dict objects, one for each item. Returns the file system path to the template file.
Required for passing the file to an external process
""" """
return [] template = self.label.name
template = template.replace('/', os.path.sep)
template = template.replace('\\', os.path.sep)
def render_to_file(self, filename, items, **kwargs): template = os.path.join(settings.MEDIA_ROOT, template)
return template
def get_context_data(self, request):
""" """
Render labels to a PDF file Supply custom context data to the template for rendering.
Note: Override this in any subclass
""" """
records = self.get_record_data(items) return {}
writer = LabelWriter(self.template) def context(self, request):
writer.write_labels(records, filename)
def render(self, items, **kwargs):
""" """
Render labels to an in-memory PDF object, and return it Provides context data to the template.
""" """
records = self.get_record_data(items) context = self.get_context_data(request)
writer = LabelWriter(self.template) # Add "basic" context data which gets passed to every label
context['base_url'] = common.models.InvenTreeSetting.get_setting('INVENTREE_BASE_URL')
context['date'] = datetime.datetime.now().date()
context['datetime'] = datetime.datetime.now()
context['request'] = request
context['user'] = request.user
context['width'] = self.width
context['height'] = self.height
buffer = io.BytesIO() return context
writer.write_labels(records, buffer) def render_as_string(self, request, **kwargs):
"""
Render the label to a HTML string
return buffer Useful for debug mode (viewing generated code)
"""
return render_to_string(self.template_name, self.context(request), request)
def render(self, request, **kwargs):
"""
Render the label template to a PDF file
Uses django-weasyprint plugin to render HTML template
"""
wp = WeasyprintLabelMixin(
request,
self.template_name,
base_url=request.build_absolute_uri("/"),
presentational_hints=True,
**kwargs
)
return wp.render_to_response(
self.context(request),
**kwargs
)
class StockItemLabel(LabelTemplate): class StockItemLabel(LabelTemplate):
@ -157,29 +241,24 @@ class StockItemLabel(LabelTemplate):
return items.exists() return items.exists()
def get_record_data(self, items): def get_context_data(self, request):
""" """
Generate context data for each provided StockItem Generate context data for each provided StockItem
""" """
records = []
for item in items: stock_item = self.object_to_print
# Add some basic information return {
records.append({ 'item': stock_item,
'item': item, 'part': stock_item.part,
'part': item.part, 'name': stock_item.part.full_name,
'name': item.part.name, 'ipn': stock_item.part.IPN,
'ipn': item.part.IPN, 'quantity': normalize(stock_item.quantity),
'quantity': normalize(item.quantity), 'serial': stock_item.serial,
'serial': item.serial, 'uid': stock_item.uid,
'uid': item.uid, 'qr_data': stock_item.format_barcode(brief=True),
'pk': item.pk, 'tests': stock_item.testResultMap()
'qr_data': item.format_barcode(brief=True), }
'tests': item.testResultMap()
})
return records
class StockLocationLabel(LabelTemplate): class StockLocationLabel(LabelTemplate):
@ -212,17 +291,14 @@ class StockLocationLabel(LabelTemplate):
return locs.exists() return locs.exists()
def get_record_data(self, locations): def get_context_data(self, request):
""" """
Generate context data for each provided StockLocation Generate context data for each provided StockLocation
""" """
records = [] location = self.object_to_print
for loc in locations: return {
'location': location,
records.append({ 'qr_data': location.format_barcode(brief=True),
'location': loc, }
})
return records

View File

@ -0,0 +1,28 @@
{% load report %}
{% load barcode %}
<head>
<style>
@page {
size: {{ width }}mm {{ height }}mm;
{% block margin %}
margin: 0mm;
{% endblock %}
}
img {
display: inline-block;
image-rendering: pixelated;
}
{% block style %}
{% endblock %}
</style>
</head>
<body>
{% block content %}
<!-- Label data rendered here! -->
{% endblock %}
</body>

View File

@ -0,0 +1,20 @@
{% extends "label/label_base.html" %}
{% load barcode %}
{% block style %}
.qr {
position: fixed;
left: 0mm;
top: 0mm;
height: {{ height }}mm;
width: {{ height }}mm;
}
{% endblock %}
{% block content %}
<img class='qr' src='{% qrcode qr_data %}'>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "label/label_base.html" %}
{% load barcode %}
{% block style %}
.qr {
position: fixed;
left: 0mm;
top: 0mm;
height: {{ height }}mm;
width: {{ height }}mm;
}
{% endblock %}
{% block content %}
<img class='qr' src='{% qrcode qr_data %}'>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "label/label_base.html" %}
{% load barcode %}
{% block style %}
.qr {
position: fixed;
left: 0mm;
top: 0mm;
height: {{ height }}mm;
width: {{ height }}mm;
}
.loc {
font-family: Arial, Helvetica, sans-serif;
display: inline;
position: absolute;
left: {{ height }}mm;
top: 2mm;
}
{% endblock %}
{% block content %}
<img class='qr' src='{% qrcode qr_data %}'>
<div class='loc'>
{{ location.name }}
</div>
{% endblock %}

View File

@ -1,16 +0,0 @@
<style>
@page {
width: 24mm;
height: 24mm;
padding: 1mm;
}
.qr {
margin: 2px;
width: 22mm;
height: 22mm;
}
</style>
<img class='qr' src="{{ label_tools.qr_code(item.barcode) }}"/>

View File

@ -1,16 +0,0 @@
<style>
@page {
width: 24mm;
height: 24mm;
padding: 1mm;
}
.qr {
margin: 2px;
width: 22mm;
height: 22mm;
}
</style>
<img class='qr' src="{{ label_tools.qr_code(location.barcode) }}"/>

View File

@ -1,43 +0,0 @@
<style>
@page {
width: 75mm;
height: 24mm;
padding: 1mm;
}
.location {
padding: 5px;
font-weight: bold;
font-family: Arial, Helvetica, sans-serif;
height: 100%;
vertical-align: middle;
float: right;
display: inline;
font-size: 125%;
position: absolute;
top: 0mm;
left: 23mm;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.qr {
margin: 2px;
width: 22mm;
height: 22mm;
}
</style>
<img class='qr' src="{{ label_tools.qr_code(location.barcode) }}"/>
<div class='location'>
{{ location.name }}
<br>
<br>
<hr>
Location ID: {{ location.pk }}
</div>
</div>

View File

@ -270,14 +270,12 @@ class PartQRTest(PartViewTestCase):
data = str(response.content) data = str(response.content)
self.assertIn('Part QR Code', data) self.assertIn('Part QR Code', data)
self.assertIn('<img class=', data) self.assertIn('<img src=', data)
def test_invalid_part(self): def test_invalid_part(self):
response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
data = str(response.content) self.assertEqual(response.status_code, 200)
self.assertIn('Error:', data)
class CategoryTest(PartViewTestCase): class CategoryTest(PartViewTestCase):

View File

@ -164,7 +164,7 @@ class ReportPrintMixin:
report.object_to_print = item report.object_to_print = item
if debug_mode: if debug_mode:
outputs.append(report.render_to_string(request)) outputs.append(report.render_as_string(request))
else: else:
outputs.append(report.render(request)) outputs.append(report.render(request))

View File

@ -221,7 +221,7 @@ class ReportTemplateBase(ReportBase):
return context return context
def render_to_string(self, request, **kwargs): def render_as_string(self, request, **kwargs):
""" """
Render the report to a HTML stiring. Render the report to a HTML stiring.

View File

@ -0,0 +1,81 @@
"""
Template tags for rendering various barcodes
"""
import base64
from io import BytesIO
from django import template
import qrcode as python_qrcode
import barcode as python_barcode
register = template.Library()
def image_data(img, fmt='PNG'):
"""
Convert an image into HTML renderable data
Returns a string ``data:image/FMT;base64,xxxxxxxxx`` which can be rendered to an <img> tag
"""
buffered = BytesIO()
img.save(buffered, format=fmt)
img_str = base64.b64encode(buffered.getvalue())
return f"data:image/{fmt.lower()};charset=utf-8;base64," + img_str.decode()
@register.simple_tag()
def qrcode(data, **kwargs):
"""
Return a byte-encoded QR code image
Optional kwargs
---------------
fill_color: Fill color (default = black)
back_color: Background color (default = white)
"""
# Construct "default" values
params = dict(
box_size=20,
border=1,
)
fill_color = kwargs.pop('fill_color', 'black')
back_color = kwargs.pop('back_color', 'white')
params.update(**kwargs)
qr = python_qrcode.QRCode(**params)
qr.add_data(data, optimize=20)
qr.make(fit=True)
qri = qr.make_image(fill_color=fill_color, back_color=back_color)
return image_data(qri)
@register.simple_tag()
def barcode(data, barcode_class='code128', **kwargs):
"""
Render a barcode
"""
constructor = python_barcode.get_barcode_class(barcode_class)
data = str(data).zfill(constructor.digits)
writer = python_barcode.writer.ImageWriter
barcode_image = constructor(data, writer=writer())
image = barcode_image.render(writer_options=kwargs)
# Render to byte-encoded PNG
return image_data(image)

View File

@ -105,9 +105,6 @@ class StockItemTest(StockViewTestCase):
response = self.client.get(reverse('stock-item-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest') response = self.client.get(reverse('stock-item-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
data = str(response.content)
self.assertIn('Error:', data)
def test_adjust_items(self): def test_adjust_items(self):
url = reverse('stock-adjust') url = reverse('stock-adjust')

View File

@ -114,6 +114,18 @@ function selectLabel(labels, items, options={}) {
* (via AJAX) from the server. * (via AJAX) from the server.
*/ */
// If only a single label template is provided,
// just run with that!
if (labels.length == 1) {
if (options.success) {
options.success(labels[0].pk);
}
return;
}
var modal = options.modal || '#modal-form'; var modal = options.modal || '#modal-form';
var label_list = makeOptionsList( var label_list = makeOptionsList(

View File

@ -1,12 +1,14 @@
{% load qr_code %} {% load barcode %}
{% load i18n %}
<div class='container' style='width: 80%;'> <div class='container' style='width: 80%;'>
{% if qr_data %} {% if qr_data %}
<div class='qr-container'> <div class='qr-container'>
<img class='qr-code' src="{% qr_url_from_text qr_data size='m' image_format='png' error_correction='m' %}" alt="QR Code"> <img src="{% qrcode qr_data %}">
</div> </div>
{% else %} {% else %}
<b>Error:</b><br> <div class='alert alert-block alert-warning'>
{{ error_msg }} {% trans "QR data not provided" %}
</div>
{% endif %} {% endif %}
</div> </div>

View File

@ -1,7 +1,6 @@
wheel>=0.34.2 # Wheel wheel>=0.34.2 # Wheel
Django==3.0.7 # Django package Django==3.0.7 # Django package
pillow==7.1.0 # Image manipulation pillow==7.1.0 # Image manipulation
blabel==0.1.3 # Simple PDF label printing
djangorestframework==3.10.3 # DRF framework djangorestframework==3.10.3 # DRF framework
django-dbbackup==3.3.0 # Database backup / restore functionality django-dbbackup==3.3.0 # Database backup / restore functionality
django-cors-headers==3.2.0 # CORS headers extension for DRF django-cors-headers==3.2.0 # CORS headers extension for DRF
@ -16,7 +15,6 @@ tablib==0.13.0 # Import / export data files
django-crispy-forms==1.8.1 # Form helpers django-crispy-forms==1.8.1 # Form helpers
django-import-export==2.0.0 # Data import / export for admin interface django-import-export==2.0.0 # Data import / export for admin interface
django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files django-cleanup==5.1.0 # Manage deletion of old / unused uploaded files
django-qr-code==1.2.0 # Generate QR codes
flake8==3.8.3 # PEP checking flake8==3.8.3 # PEP checking
pep8-naming==0.11.1 # PEP naming convention extension pep8-naming==0.11.1 # PEP naming convention extension
coverage==5.3 # Unit test coverage coverage==5.3 # Unit test coverage
@ -31,5 +29,7 @@ certifi # Certifi is (most likely) installed through one
django-error-report==0.2.0 # Error report viewer for the admin interface django-error-report==0.2.0 # Error report viewer for the admin interface
django-test-migrations==1.1.0 # Unit testing for database migrations django-test-migrations==1.1.0 # Unit testing for database migrations
django-migration-linter==2.5.0 # Linting checks for database migrations django-migration-linter==2.5.0 # Linting checks for database migrations
python-barcode[images]==0.13.1 # Barcode generator
qrcode[pil]==6.1 # QR code generator
inventree # Install the latest version of the InvenTree API python library inventree # Install the latest version of the InvenTree API python library