FEAT(Asset): Add ability to generate whole page of labels

This commit is contained in:
2021-12-31 12:17:46 +00:00
parent fdbdaab52e
commit 484f155e43
10 changed files with 150 additions and 27 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -114,10 +114,8 @@ def orderby(request, field, attr):
return dict_.urlencode() return dict_.urlencode()
# Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist
@register.filter(needs_autoescape=True)
def get_field(obj, field, autoescape=True): def get_field(obj, field, autoescape=True):
value = getattr(obj, field) value = getattr(obj, field)
if(isinstance(value, bool)): if(isinstance(value, bool)):
@@ -221,7 +219,7 @@ def button(type, url=None, pk=None, clazz="", icon=None, text="", id=None, style
return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style} return {'target': url, 'pk': pk, 'class': clazz, 'icon': icon, 'text': text, 'id': id, 'style': style}
@register.simple_tag @register.simple_tag # TODO Can these be done with annotation/aggregation?
def invoices_waiting(): def invoices_waiting():
return len(models.Event.objects.waiting_invoices()) return len(models.Event.objects.waiting_invoices())

View File

@@ -1,3 +1,6 @@
import urllib.parse
class AssetIDConverter: # Forces lowercase to uppercase class AssetIDConverter: # Forces lowercase to uppercase
regex = '[^/]+' regex = '[^/]+'
@@ -6,3 +9,16 @@ class AssetIDConverter: # Forces lowercase to uppercase
def to_url(self, value): def to_url(self, value):
return str(value).upper() return str(value).upper()
class ListConverter:
regex = '[^/]+'
def to_python(self, value):
return value.split(',')
def to_url(self, value):
string = ""
for i in value:
string += "," + str(i)
return string[1:]

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -1,6 +1,7 @@
{% extends 'base_assets.html' %} {% extends 'base_assets.html' %}
{% load paginator from filters %} {% load paginator from filters %}
{% load button from filters %} {% load button from filters %}
{% load ids_from_objects from asset_tags %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% load static %} {% load static %}
@@ -85,6 +86,9 @@
<div class="row my-2"> <div class="row my-2">
<div class="col text-right px-0"> <div class="col text-right px-0">
{% button 'new' 'asset_create' style="width: 6em" %} {% button 'new' 'asset_create' style="width: 6em" %}
{% if object_list %}
<a class="btn btn-primary" href="{% url 'generate_labels' object_list|ids_from_objects %}"><span class="fas fa-barcode"></span> Generate Labels</a>
{% endif %}
</div> </div>
</div> </div>
<div class="row my-2"> <div class="row my-2">

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE document SYSTEM "rml.dtd">
{% load multiply from filters %}
{% load index from asset_tags %}
<document filename="{{filename}}">
<template>
<pageTemplate id="main">
<pageGraphics>
</pageGraphics>
<frame id="first" x1="5" y1="-10" width="581" height="842"/>
</pageTemplate>
</template>
<stylesheet>
<blockTableStyle id="table">
<!-- show a grid: this also comes in handy for debugging your tables.-->
<lineStyle kind="GRID" colorName="black" thickness="1" start="0,0" stop="-1,-1" />
</blockTableStyle>
</stylesheet>
<story>
<blockTable style="table">
{% for i in images0 %}
<tr>
<td>{% with images0|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images1|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images2|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
<td>{% with images3|index:forloop.counter0 as image %}{% if image %}<illustration width="120" height="35"><image file="data:image/png;base64,{{image}}" x="0" y="0"
width="120" height="35"/></illustration>{% endif %}{% endwith %}</td>
</tr>
{% endfor %}
</blockTable>
</story>
</document>

View File

@@ -12,9 +12,7 @@
{% button 'edit' url='asset_update' pk=object.asset_id %} {% button 'edit' url='asset_update' pk=object.asset_id %}
{% button 'duplicate' url='asset_duplicate' pk=object.asset_id %} {% button 'duplicate' url='asset_duplicate' pk=object.asset_id %}
<a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><span class="fas fa-certificate"></span> Audit</a> <a type="button" class="btn btn-info" href="{% url 'asset_audit' object.asset_id %}"><span class="fas fa-certificate"></span> Audit</a>
{% if object.is_cable %}
<a type="button" class="btn btn-primary" href="{% url 'generate_label' object.asset_id %}"><span class="fas fa-barcode"></span> Generate Label</a> <a type="button" class="btn btn-primary" href="{% url 'generate_label' object.asset_id %}"><span class="fas fa-barcode"></span> Generate Label</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}
{% if create or edit or duplicate %} {% if create or edit or duplicate %}

View File

@@ -0,0 +1,17 @@
from django import template
from assets import models
register = template.Library()
@register.filter
def ids_from_objects(object_list):
id_list = []
for obj in object_list:
id_list.append(obj.asset_id)
return id_list
@register.filter
def index(indexable, i):
return indexable[i] if i < len(indexable) else None

View File

@@ -7,6 +7,7 @@ from PyRIGS.views import OEmbedView
from . import views, converters from . import views, converters
register_converter(converters.AssetIDConverter, 'asset') register_converter(converters.AssetIDConverter, 'asset')
register_converter(converters.ListConverter, 'list')
urlpatterns = [ urlpatterns = [
path('', login_required(views.AssetList.as_view()), name='asset_index'), path('', login_required(views.AssetList.as_view()), name='asset_index'),
@@ -19,6 +20,7 @@ urlpatterns = [
path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset') path('asset/id/<asset:pk>/duplicate/', permission_required_with_403('assets.add_asset')
(views.AssetDuplicate.as_view()), name='asset_duplicate'), (views.AssetDuplicate.as_view()), name='asset_duplicate'),
path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'), path('asset/id/<asset:pk>/label', login_required(views.GenerateLabel.as_view()), name='generate_label'),
path('asset/<list:ids>/list/label', views.GenerateLabels.as_view(), name='generate_labels'),
path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'), path('cabletype/list/', login_required(views.CableTypeList.as_view()), name='cable_type_list'),
path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'), path('cabletype/create/', permission_required_with_403('assets.add_cable_type')(views.CableTypeCreate.as_view()), name='cable_type_create'),

View File

@@ -1,5 +1,8 @@
import simplejson import simplejson
import random import random
import base64
from io import BytesIO
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core import serializers from django.core import serializers
@@ -11,10 +14,13 @@ from django.utils.decorators import method_decorator
from django.views import generic from django.views import generic
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template.loader import get_template
from PyPDF2 import PdfFileMerger, PdfFileReader
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from barcode import Code39 from barcode import Code39
from barcode.writer import ImageWriter from barcode.writer import ImageWriter
from z3c.rml import rml2pdf
from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \ from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \
is_ajax, OEmbedView is_ajax, OEmbedView
@@ -349,35 +355,82 @@ class CableTypeUpdate(generic.UpdateView):
return reverse("cable_type_detail", kwargs={"pk": self.object.pk}) return reverse("cable_type_detail", kwargs={"pk": self.object.pk})
class GenerateLabel(generic.View): def generate_label(pk):
def get(self, request, pk): black = (0, 0, 0)
black = (0, 0, 0) white = (255, 255, 255)
white = (255, 255, 255) size = (700, 200)
size = (700, 200) font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20)
font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20) obj = get_object_or_404(models.Asset, asset_id=pk)
obj = get_object_or_404(models.Asset, asset_id=pk)
asset_id = "Asset: {}".format(obj.asset_id) asset_id = "Asset: {}".format(obj.asset_id)
if obj.is_cable:
length = "Length: {}m".format(obj.length) length = "Length: {}m".format(obj.length)
csa = "CSA: {}mm²".format(obj.csa) csa = "CSA: {}mm²".format(obj.csa)
image = Image.new("RGB", size, white) image = Image.new("RGB", size, white)
logo = Image.open("static/imgs/square_logo.png") logo = Image.open("static/imgs/square_logo.png")
draw = ImageDraw.Draw(image) draw = ImageDraw.Draw(image)
draw.text((210, 140), asset_id, fill=black, font=font) draw.text((210, 140), asset_id, fill=black, font=font)
if obj.is_cable:
draw.text((210, 170), length, fill=black, font=font) draw.text((210, 170), length, fill=black, font=font)
draw.text((350, 170), csa, fill=black, font=font) draw.text((360, 170), csa, fill=black, font=font)
draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font) draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", fill=black, font=font)
barcode = Code39(str(obj.asset_id), writer=ImageWriter()) barcode = Code39(str(obj.asset_id), writer=ImageWriter())
logo_size = (200, 200) logo_size = (200, 200)
image.paste(logo.resize(logo_size, Image.ANTIALIAS)) image.paste(logo.resize(logo_size, Image.ANTIALIAS))
barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False}) barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False})
width, height = barcode_image.size width, height = barcode_image.size
image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0)) image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0))
return image
class GenerateLabel(generic.View): # TODO Caching
def get(self, request, pk):
response = HttpResponse(content_type="image/png") response = HttpResponse(content_type="image/png")
image.save(response, "PNG") generate_label(pk).save(response, "PNG")
return response
class GenerateLabels(generic.View):
def get(self, request, ids):
response = HttpResponse(content_type='application/pdf')
template = get_template('labels_print.xml')
images = []
for asset_id in ids:
image = generate_label(asset_id)
in_mem_file = BytesIO()
image.save(in_mem_file, format="PNG")
# reset file pointer to start
in_mem_file.seek(0)
img_bytes = in_mem_file.read()
base64_encoded_result_bytes = base64.b64encode(img_bytes)
base64_encoded_result_str = base64_encoded_result_bytes.decode('ascii')
images.append(base64_encoded_result_str)
context = {
'images0': images[::4],
'images1': images[1::4],
'images2': images[2::4],
'images3': images[3::4],
'filename': "Asset Label Sheet generated at {}".format(timezone.now())
}
merger = PdfFileMerger()
rml = template.render(context)
buffer = rml2pdf.parseString(rml)
merger.append(PdfFileReader(buffer))
buffer.close()
merged = BytesIO()
merger.write(merged)
response['Content-Disposition'] = 'filename="{}"'.format(context['filename'])
response.write(merged.getvalue())
return response return response