mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
Merge pull request #1342 from SchrodingersGat/label-improvements
Label improvements
This commit is contained in:
commit
b8327a5531
@ -204,7 +204,6 @@ INSTALLED_APPS = [
|
||||
'crispy_forms', # Improved form rendering
|
||||
'import_export', # Import / export tables to file
|
||||
'django_cleanup.apps.CleanupConfig', # Automatically delete orphaned MEDIA files
|
||||
'qr_code', # Generate QR codes
|
||||
'mptt', # Modified Preorder Tree Traversal
|
||||
'markdownx', # Markdown editing
|
||||
'markdownify', # Markdown template rendering
|
||||
@ -250,6 +249,7 @@ TEMPLATES = [
|
||||
os.path.join(BASE_DIR, 'templates'),
|
||||
# Allow templates in the reporting directory to be accessed
|
||||
os.path.join(MEDIA_ROOT, 'report'),
|
||||
os.path.join(MEDIA_ROOT, 'label'),
|
||||
],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
@ -407,8 +407,6 @@ CACHES = {
|
||||
}
|
||||
}
|
||||
|
||||
QR_CODE_CACHE_ALIAS = 'qr-code'
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators
|
||||
|
||||
|
@ -125,6 +125,7 @@
|
||||
.qr-container {
|
||||
width: 100%;
|
||||
align-content: center;
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
|
@ -9,7 +9,6 @@ from django.conf.urls import url, include
|
||||
from django.urls import path
|
||||
from django.contrib import admin
|
||||
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 supplier_part_urls
|
||||
@ -21,7 +20,7 @@ from stock.urls import stock_urls
|
||||
from build.urls import build_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 part.api import part_api_urls, bom_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/', 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'^search/', SearchView.as_view(), name='search'),
|
||||
url(r'^stats/', DatabaseStatsView.as_view(), name='stats'),
|
||||
|
@ -12,7 +12,7 @@ from rest_framework.views import APIView
|
||||
from stock.models import StockItem
|
||||
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):
|
@ -5,7 +5,7 @@ import hashlib
|
||||
import logging
|
||||
|
||||
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.serializers import StockItemSerializer, LocationSerializer
|
@ -4,7 +4,7 @@
|
||||
DigiKey barcode decoding
|
||||
"""
|
||||
|
||||
from barcode.barcode import BarcodePlugin
|
||||
from barcodes.barcode import BarcodePlugin
|
||||
|
||||
|
||||
class DigikeyBarcodePlugin(BarcodePlugin):
|
@ -13,7 +13,7 @@ references model objects actually exist in the database.
|
||||
|
||||
import json
|
||||
|
||||
from barcode.barcode import BarcodePlugin
|
||||
from barcodes.barcode import BarcodePlugin
|
||||
|
||||
from stock.models import StockItem, StockLocation
|
||||
from part.models import Part
|
@ -1,11 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.conf.urls import url, include
|
||||
from django.core.exceptions import ValidationError, FieldError
|
||||
from django.http import HttpResponse
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
|
||||
@ -13,6 +12,7 @@ from rest_framework import generics, filters
|
||||
from rest_framework.response import Response
|
||||
|
||||
import InvenTree.helpers
|
||||
import common.models
|
||||
|
||||
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:
|
||||
"""
|
||||
Mixin for extracting stock items from query params
|
||||
@ -158,7 +226,7 @@ class StockItemLabelDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = StockItemLabelSerializer
|
||||
|
||||
|
||||
class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin):
|
||||
class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin, LabelPrintMixin):
|
||||
"""
|
||||
API endpoint for printing a StockItemLabel object
|
||||
"""
|
||||
@ -173,34 +241,7 @@ class StockItemLabelPrint(generics.RetrieveAPIView, StockItemLabelMixin):
|
||||
|
||||
items = self.get_items()
|
||||
|
||||
if len(items) == 0:
|
||||
# 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'
|
||||
)
|
||||
return self.print(request, items)
|
||||
|
||||
|
||||
class StockLocationLabelMixin:
|
||||
@ -320,7 +361,7 @@ class StockLocationLabelDetail(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = StockLocationLabelSerializer
|
||||
|
||||
|
||||
class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin):
|
||||
class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin, LabelPrintMixin):
|
||||
"""
|
||||
API endpoint for printing a StockLocationLabel object
|
||||
"""
|
||||
@ -332,35 +373,7 @@ class StockLocationLabelPrint(generics.RetrieveAPIView, StockLocationLabelMixin)
|
||||
|
||||
locations = self.get_locations()
|
||||
|
||||
if len(locations) == 0:
|
||||
# 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'
|
||||
)
|
||||
return self.print(request, locations)
|
||||
|
||||
|
||||
label_api_urls = [
|
||||
|
@ -1,6 +1,7 @@
|
||||
import os
|
||||
import shutil
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
@ -9,6 +10,20 @@ from django.conf import settings
|
||||
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):
|
||||
name = 'label'
|
||||
|
||||
@ -35,6 +50,7 @@ class LabelConfig(AppConfig):
|
||||
src_dir = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'templates',
|
||||
'label',
|
||||
'stockitem',
|
||||
)
|
||||
|
||||
@ -54,6 +70,8 @@ class LabelConfig(AppConfig):
|
||||
'file': 'qr.html',
|
||||
'name': 'QR Code',
|
||||
'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'])
|
||||
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}'")
|
||||
shutil.copyfile(src_file, dst_file)
|
||||
|
||||
@ -86,7 +118,9 @@ class LabelConfig(AppConfig):
|
||||
description=label['description'],
|
||||
label=filename,
|
||||
filters='',
|
||||
enabled=True
|
||||
enabled=True,
|
||||
width=label['width'],
|
||||
height=label['height'],
|
||||
)
|
||||
except:
|
||||
pass
|
||||
@ -106,6 +140,7 @@ class LabelConfig(AppConfig):
|
||||
src_dir = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)),
|
||||
'templates',
|
||||
'label',
|
||||
'stocklocation',
|
||||
)
|
||||
|
||||
@ -125,11 +160,15 @@ class LabelConfig(AppConfig):
|
||||
'file': 'qr.html',
|
||||
'name': 'QR Code',
|
||||
'description': 'Simple QR code label',
|
||||
'width': 24,
|
||||
'height': 24,
|
||||
},
|
||||
{
|
||||
'file': 'qr_and_text.html',
|
||||
'name': 'QR and text',
|
||||
'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'])
|
||||
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}'")
|
||||
shutil.copyfile(src_file, dst_file)
|
||||
|
||||
@ -162,7 +215,9 @@ class LabelConfig(AppConfig):
|
||||
description=label['description'],
|
||||
label=filename,
|
||||
filters='',
|
||||
enabled=True
|
||||
enabled=True,
|
||||
width=label['width'],
|
||||
height=label['height'],
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
34
InvenTree/label/migrations/0006_auto_20210222_1535.py
Normal file
34
InvenTree/label/migrations/0006_auto_20210222_1535.py
Normal 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]'),
|
||||
),
|
||||
]
|
@ -5,21 +5,35 @@ Label printing models
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
import os
|
||||
import io
|
||||
|
||||
from blabel import LabelWriter
|
||||
import logging
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
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.template.loader import render_to_string
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from InvenTree.helpers import validateFilterString, normalize
|
||||
|
||||
import common.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):
|
||||
""" Place the label file into the correct subdirectory """
|
||||
@ -43,6 +57,21 @@ def validate_stock_location_filters(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):
|
||||
"""
|
||||
Base class for generic, filterable labels.
|
||||
@ -53,6 +82,9 @@ class LabelTemplate(models.Model):
|
||||
|
||||
# Each class of label files will be stored in a separate subdirectory
|
||||
SUBDIR = "label"
|
||||
|
||||
# Object we will be printing against (will be filled out later)
|
||||
object_to_print = None
|
||||
|
||||
@property
|
||||
def template(self):
|
||||
@ -92,38 +124,90 @@ class LabelTemplate(models.Model):
|
||||
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)
|
||||
|
||||
writer.write_labels(records, filename)
|
||||
|
||||
def render(self, items, **kwargs):
|
||||
def context(self, request):
|
||||
"""
|
||||
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):
|
||||
@ -157,29 +241,24 @@ class StockItemLabel(LabelTemplate):
|
||||
|
||||
return items.exists()
|
||||
|
||||
def get_record_data(self, items):
|
||||
def get_context_data(self, request):
|
||||
"""
|
||||
Generate context data for each provided StockItem
|
||||
"""
|
||||
records = []
|
||||
|
||||
for item in items:
|
||||
|
||||
# Add some basic information
|
||||
records.append({
|
||||
'item': item,
|
||||
'part': item.part,
|
||||
'name': item.part.name,
|
||||
'ipn': item.part.IPN,
|
||||
'quantity': normalize(item.quantity),
|
||||
'serial': item.serial,
|
||||
'uid': item.uid,
|
||||
'pk': item.pk,
|
||||
'qr_data': item.format_barcode(brief=True),
|
||||
'tests': item.testResultMap()
|
||||
})
|
||||
stock_item = self.object_to_print
|
||||
|
||||
return records
|
||||
return {
|
||||
'item': stock_item,
|
||||
'part': stock_item.part,
|
||||
'name': stock_item.part.full_name,
|
||||
'ipn': stock_item.part.IPN,
|
||||
'quantity': normalize(stock_item.quantity),
|
||||
'serial': stock_item.serial,
|
||||
'uid': stock_item.uid,
|
||||
'qr_data': stock_item.format_barcode(brief=True),
|
||||
'tests': stock_item.testResultMap()
|
||||
}
|
||||
|
||||
|
||||
class StockLocationLabel(LabelTemplate):
|
||||
@ -212,17 +291,14 @@ class StockLocationLabel(LabelTemplate):
|
||||
|
||||
return locs.exists()
|
||||
|
||||
def get_record_data(self, locations):
|
||||
def get_context_data(self, request):
|
||||
"""
|
||||
Generate context data for each provided StockLocation
|
||||
"""
|
||||
|
||||
records = []
|
||||
|
||||
for loc in locations:
|
||||
location = self.object_to_print
|
||||
|
||||
records.append({
|
||||
'location': loc,
|
||||
})
|
||||
|
||||
return records
|
||||
return {
|
||||
'location': location,
|
||||
'qr_data': location.format_barcode(brief=True),
|
||||
}
|
||||
|
28
InvenTree/label/templates/label/label_base.html
Normal file
28
InvenTree/label/templates/label/label_base.html
Normal 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>
|
20
InvenTree/label/templates/label/stockitem/qr.html
Normal file
20
InvenTree/label/templates/label/stockitem/qr.html
Normal 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 %}
|
20
InvenTree/label/templates/label/stocklocation/qr.html
Normal file
20
InvenTree/label/templates/label/stocklocation/qr.html
Normal 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 %}
|
@ -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 %}
|
@ -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) }}"/>
|
@ -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) }}"/>
|
@ -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>
|
||||
|
@ -270,14 +270,12 @@ class PartQRTest(PartViewTestCase):
|
||||
data = str(response.content)
|
||||
|
||||
self.assertIn('Part QR Code', data)
|
||||
self.assertIn('<img class=', data)
|
||||
self.assertIn('<img src=', data)
|
||||
|
||||
def test_invalid_part(self):
|
||||
response = self.client.get(reverse('part-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
|
||||
data = str(response.content)
|
||||
|
||||
self.assertIn('Error:', data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class CategoryTest(PartViewTestCase):
|
||||
|
@ -164,7 +164,7 @@ class ReportPrintMixin:
|
||||
report.object_to_print = item
|
||||
|
||||
if debug_mode:
|
||||
outputs.append(report.render_to_string(request))
|
||||
outputs.append(report.render_as_string(request))
|
||||
else:
|
||||
outputs.append(report.render(request))
|
||||
|
||||
|
@ -221,7 +221,7 @@ class ReportTemplateBase(ReportBase):
|
||||
|
||||
return context
|
||||
|
||||
def render_to_string(self, request, **kwargs):
|
||||
def render_as_string(self, request, **kwargs):
|
||||
"""
|
||||
Render the report to a HTML stiring.
|
||||
|
||||
|
81
InvenTree/report/templatetags/barcode.py
Normal file
81
InvenTree/report/templatetags/barcode.py
Normal 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)
|
@ -105,9 +105,6 @@ class StockItemTest(StockViewTestCase):
|
||||
response = self.client.get(reverse('stock-item-qr', args=(9999,)), HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
data = str(response.content)
|
||||
self.assertIn('Error:', data)
|
||||
|
||||
def test_adjust_items(self):
|
||||
url = reverse('stock-adjust')
|
||||
|
||||
|
@ -114,6 +114,18 @@ function selectLabel(labels, items, options={}) {
|
||||
* (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 label_list = makeOptionsList(
|
||||
|
@ -1,12 +1,14 @@
|
||||
{% load qr_code %}
|
||||
{% load barcode %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class='container' style='width: 80%;'>
|
||||
{% if qr_data %}
|
||||
<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>
|
||||
{% else %}
|
||||
<b>Error:</b><br>
|
||||
{{ error_msg }}
|
||||
<div class='alert alert-block alert-warning'>
|
||||
{% trans "QR data not provided" %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -1,7 +1,6 @@
|
||||
wheel>=0.34.2 # Wheel
|
||||
Django==3.0.7 # Django package
|
||||
pillow==7.1.0 # Image manipulation
|
||||
blabel==0.1.3 # Simple PDF label printing
|
||||
djangorestframework==3.10.3 # DRF framework
|
||||
django-dbbackup==3.3.0 # Database backup / restore functionality
|
||||
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-import-export==2.0.0 # Data import / export for admin interface
|
||||
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
|
||||
pep8-naming==0.11.1 # PEP naming convention extension
|
||||
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-test-migrations==1.1.0 # Unit testing 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
|
||||
|
Loading…
Reference in New Issue
Block a user