diff --git a/RIGS/static/imgs/square_logo.png b/RIGS/static/imgs/square_logo.png deleted file mode 100644 index 3c2143ba..00000000 Binary files a/RIGS/static/imgs/square_logo.png and /dev/null differ diff --git a/RIGS/templatetags/filters.py b/RIGS/templatetags/filters.py index 8a42a568..7efd42bb 100644 --- a/RIGS/templatetags/filters.py +++ b/RIGS/templatetags/filters.py @@ -114,10 +114,8 @@ def orderby(request, field, attr): return dict_.urlencode() -# Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist - -@register.filter(needs_autoescape=True) +@register.filter(needs_autoescape=True) # Used for accessing outside of a form, i.e. in detail views of RiskAssessment and EventChecklist def get_field(obj, field, autoescape=True): value = getattr(obj, field) 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} -@register.simple_tag +@register.simple_tag # TODO Can these be done with annotation/aggregation? def invoices_waiting(): return len(models.Event.objects.waiting_invoices()) diff --git a/assets/converters.py b/assets/converters.py index da55d2e6..e57c56bd 100644 --- a/assets/converters.py +++ b/assets/converters.py @@ -1,3 +1,6 @@ +import urllib.parse + + class AssetIDConverter: # Forces lowercase to uppercase regex = '[^/]+' @@ -6,3 +9,16 @@ class AssetIDConverter: # Forces lowercase to uppercase def to_url(self, value): 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:] diff --git a/assets/static/imgs/square_logo.png b/assets/static/imgs/square_logo.png new file mode 100644 index 00000000..445184da Binary files /dev/null and b/assets/static/imgs/square_logo.png differ diff --git a/assets/templates/asset_list.html b/assets/templates/asset_list.html index c89d1bc3..d335fad7 100644 --- a/assets/templates/asset_list.html +++ b/assets/templates/asset_list.html @@ -1,6 +1,7 @@ {% extends 'base_assets.html' %} {% load paginator from filters %} {% load button from filters %} +{% load ids_from_objects from asset_tags %} {% load widget_tweaks %} {% load static %} @@ -85,6 +86,9 @@
{% button 'new' 'asset_create' style="width: 6em" %} + {% if object_list %} + Generate Labels + {% endif %}
diff --git a/assets/templates/labels_print.xml b/assets/templates/labels_print.xml new file mode 100644 index 00000000..b8adfd79 --- /dev/null +++ b/assets/templates/labels_print.xml @@ -0,0 +1,35 @@ + + +{% load multiply from filters %} +{% load index from asset_tags %} + + + + + + + + + + + {% for i in images0 %} + + {% with images0|index:forloop.counter0 as image %}{% if image %}{% endif %}{% endwith %} + {% with images1|index:forloop.counter0 as image %}{% if image %}{% endif %}{% endwith %} + {% with images2|index:forloop.counter0 as image %}{% if image %}{% endif %}{% endwith %} + {% with images3|index:forloop.counter0 as image %}{% if image %}{% endif %}{% endwith %} + + {% endfor %} + + + diff --git a/assets/templates/partials/asset_buttons.html b/assets/templates/partials/asset_buttons.html index 2551365e..71f2a11b 100644 --- a/assets/templates/partials/asset_buttons.html +++ b/assets/templates/partials/asset_buttons.html @@ -12,9 +12,7 @@ {% button 'edit' url='asset_update' pk=object.asset_id %} {% button 'duplicate' url='asset_duplicate' pk=object.asset_id %} Audit - {% if object.is_cable %} Generate Label - {% endif %}
{% endif %} {% if create or edit or duplicate %} diff --git a/assets/templatetags/asset_tags.py b/assets/templatetags/asset_tags.py new file mode 100644 index 00000000..553f646f --- /dev/null +++ b/assets/templatetags/asset_tags.py @@ -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 diff --git a/assets/urls.py b/assets/urls.py index 4ec111c1..5f45d2e8 100644 --- a/assets/urls.py +++ b/assets/urls.py @@ -7,6 +7,7 @@ from PyRIGS.views import OEmbedView from . import views, converters register_converter(converters.AssetIDConverter, 'asset') +register_converter(converters.ListConverter, 'list') urlpatterns = [ path('', login_required(views.AssetList.as_view()), name='asset_index'), @@ -19,6 +20,7 @@ urlpatterns = [ path('asset/id//duplicate/', permission_required_with_403('assets.add_asset') (views.AssetDuplicate.as_view()), name='asset_duplicate'), path('asset/id//label', login_required(views.GenerateLabel.as_view()), name='generate_label'), + path('asset//list/label', views.GenerateLabels.as_view(), name='generate_labels'), 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'), diff --git a/assets/views.py b/assets/views.py index a449112d..28894de8 100644 --- a/assets/views.py +++ b/assets/views.py @@ -1,5 +1,8 @@ import simplejson import random +import base64 +from io import BytesIO + from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.core import serializers @@ -11,10 +14,13 @@ from django.utils.decorators import method_decorator from django.views import generic from django.views.decorators.csrf import csrf_exempt 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 barcode import Code39 from barcode.writer import ImageWriter +from z3c.rml import rml2pdf from PyRIGS.views import GenericListView, GenericDetailView, GenericUpdateView, GenericCreateView, ModalURLMixin, \ is_ajax, OEmbedView @@ -349,35 +355,82 @@ class CableTypeUpdate(generic.UpdateView): return reverse("cable_type_detail", kwargs={"pk": self.object.pk}) -class GenerateLabel(generic.View): - def get(self, request, pk): - black = (0, 0, 0) - white = (255, 255, 255) - size = (700, 200) - font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20) - obj = get_object_or_404(models.Asset, asset_id=pk) +def generate_label(pk): + black = (0, 0, 0) + white = (255, 255, 255) + size = (700, 200) + font = ImageFont.truetype("static/fonts/OpenSans-Regular.tff", 20) + 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) csa = "CSA: {}mm²".format(obj.csa) - image = Image.new("RGB", size, white) - logo = Image.open("static/imgs/square_logo.png") - draw = ImageDraw.Draw(image) + image = Image.new("RGB", size, white) + logo = Image.open("static/imgs/square_logo.png") + 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((350, 170), csa, fill=black, font=font) - draw.multiline_text((500, 140), "TEC PA & Lighting\n(0115) 84 68720", 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) - barcode = Code39(str(obj.asset_id), writer=ImageWriter()) + barcode = Code39(str(obj.asset_id), writer=ImageWriter()) - logo_size = (200, 200) - image.paste(logo.resize(logo_size, Image.ANTIALIAS)) - barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False}) - width, height = barcode_image.size - image.paste(barcode_image.crop((0, 0, width, 135)), (int(((size[0] + logo_size[0]) - width) / 2), 0)) + logo_size = (200, 200) + image.paste(logo.resize(logo_size, Image.ANTIALIAS)) + barcode_image = barcode.render(writer_options={"quiet_zone": 0, "write_text": False}) + width, height = barcode_image.size + 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") - 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