mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-17 05:22:16 +00:00
FEAT(Asset): Add ability to generate whole page of labels
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
@@ -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())
|
||||||
|
|
||||||
|
|||||||
@@ -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:]
|
||||||
|
|||||||
BIN
assets/static/imgs/square_logo.png
Normal file
BIN
assets/static/imgs/square_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
@@ -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">
|
||||||
|
|||||||
35
assets/templates/labels_print.xml
Normal file
35
assets/templates/labels_print.xml
Normal 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>
|
||||||
@@ -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 %}
|
||||||
|
|||||||
17
assets/templatetags/asset_tags.py
Normal file
17
assets/templatetags/asset_tags.py
Normal 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
|
||||||
@@ -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'),
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user