229 lines
8.8 KiB
Python
229 lines
8.8 KiB
Python
import io
|
|
import re
|
|
import zipfile
|
|
import qrcode
|
|
import pandas as pd
|
|
from flask import Blueprint, render_template, request, send_file, flash, redirect, url_for
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
from blueprints.auth import login_required
|
|
from database import execute_query
|
|
from rechten import recht_vereist, heeft_recht
|
|
|
|
# definieren blueprint
|
|
qr_code_bp = Blueprint('qr_generator', __name__, template_folder='templates')
|
|
|
|
def _lettertype():
|
|
try:
|
|
# Try loading your local or system Arial font
|
|
return ImageFont.truetype("arial.ttf", 20)
|
|
except IOError:
|
|
try:
|
|
return ImageFont.load_default_font(size=20)
|
|
except AttributeError:
|
|
# Absolute fallback if Pillow is outdated, but textbbox WILL fail if this hits
|
|
return ImageFont.load_default()
|
|
|
|
|
|
def _qr_png(qr_text, top_text, bottom_text, font):
|
|
"""Eén QR-code als PNG (bytes), met optioneel tekst erboven en eronder."""
|
|
qr = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
|
|
qr.add_data(qr_text)
|
|
qr.make(fit=True)
|
|
qr_img = qr.make_image(fill_color="black", back_color="white").convert("RGB")
|
|
|
|
qr_width, qr_height = qr_img.size
|
|
padding = 40
|
|
total_height = qr_height + (2 * padding)
|
|
|
|
canvas = Image.new("RGB", (qr_width + 40, total_height + 40), "white")
|
|
draw = ImageDraw.Draw(canvas)
|
|
|
|
qr_x = (canvas.width - qr_width) // 2
|
|
qr_y = padding
|
|
canvas.paste(qr_img, (qr_x, qr_y))
|
|
|
|
if top_text:
|
|
bbox = draw.textbbox((0, 0), top_text, font=font)
|
|
text_w = bbox[2] - bbox[0]
|
|
draw.text(((canvas.width - text_w) / 2, 5), top_text, fill="black", font=font)
|
|
|
|
if bottom_text:
|
|
bbox = draw.textbbox((0, 0), bottom_text, font=font)
|
|
text_w = bbox[2] - bbox[0]
|
|
draw.text(((canvas.width - text_w) / 2, qr_y + qr_height + 5), bottom_text, fill="black", font=font)
|
|
|
|
img_io = io.BytesIO()
|
|
canvas.save(img_io, 'PNG')
|
|
return img_io.getvalue()
|
|
|
|
|
|
def generate_qr_images(excel_file, show_top, show_bottom):
|
|
# Overslaan van de header rij
|
|
df = pd.read_excel(excel_file, header=0)
|
|
|
|
zip_buffer = io.BytesIO()
|
|
font = _lettertype()
|
|
|
|
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED) as zip_file:
|
|
for index, row in df.iterrows():
|
|
# Gebruik .iloc voor veiligheid tegen kolomnamen
|
|
qr_text = str(row.iloc[0]) if not pd.isna(row.iloc[0]) else ""
|
|
top_text = str(row.iloc[1]) if not pd.isna(row.iloc[1]) else ""
|
|
bottom_text = str(row.iloc[2]) if not pd.isna(row.iloc[2]) else ""
|
|
|
|
if qr_text.strip() == "":
|
|
continue
|
|
|
|
png = _qr_png(qr_text, top_text if show_top else "", bottom_text if show_bottom else "", font)
|
|
zip_file.writestr(f"qr_{index+1}.png", png)
|
|
|
|
zip_buffer.seek(0)
|
|
return zip_buffer
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# QR-codes uit de database (tabel Toestellen + Signpost_DB)
|
|
# ---------------------------------------------------------------------------
|
|
# sleutel → (naam op het scherm, kolom in de query)
|
|
DB_VELDEN = {
|
|
'toestelnummer': ('Toestelnummer (Schooleigen_ID)', 't.Schooleigen_ID'),
|
|
'serienummer': ('Serienummer', 't.Serienummer'),
|
|
'spb': ('SPB-nummer (Signpost)', 'sp.Signpostnummer'),
|
|
'type': ('Type', 't.Type'),
|
|
}
|
|
QR_VELDEN = ('toestelnummer', 'serienummer', 'spb') # wat in de QR-code zelf mag
|
|
STATUSSEN = ('Beschikbaar', 'Uitgeleend', 'Defect')
|
|
MAX_DB_CODES = 5000
|
|
|
|
|
|
def _bestandsnaam(tekst, gebruikt):
|
|
"""Veilige, unieke bestandsnaam op basis van de QR-inhoud."""
|
|
naam = re.sub(r'[^A-Za-z0-9._-]+', '_', tekst).strip('._') or 'qr'
|
|
kandidaat, i = naam, 2
|
|
while kandidaat.lower() in gebruikt:
|
|
kandidaat, i = f"{naam}_{i}", i + 1
|
|
gebruikt.add(kandidaat.lower())
|
|
return f"{kandidaat}.png"
|
|
|
|
|
|
def toestellen_voor_qr(qr_veld, status='', zoek='', nummers=None):
|
|
"""Toestellen met een ingevulde waarde voor qr_veld, optioneel gefilterd. Geeft rijen met alle DB_VELDEN."""
|
|
kolommen = ', '.join(f"{kol} AS {sleutel}" for sleutel, (_n, kol) in DB_VELDEN.items())
|
|
qr_kol = DB_VELDEN[qr_veld][1]
|
|
sql = (f"SELECT {kolommen} FROM Toestellen t LEFT JOIN Signpost_DB sp ON sp.Toestel_ID = t.Toestel_ID "
|
|
f"WHERE {qr_kol} IS NOT NULL AND TRIM({qr_kol}) <> ''")
|
|
params = []
|
|
if status in STATUSSEN:
|
|
sql += " AND t.Status = %s"
|
|
params.append(status)
|
|
if zoek:
|
|
sql += " AND CONCAT_WS(' ', t.Schooleigen_ID, t.Serienummer, sp.Signpostnummer, t.Type) LIKE %s"
|
|
params.append(f"%{zoek}%")
|
|
if nummers:
|
|
sql += f" AND {qr_kol} IN ({', '.join(['%s'] * len(nummers))})"
|
|
params.extend(nummers)
|
|
sql += f" ORDER BY {qr_kol}"
|
|
return execute_query(sql, tuple(params)) or []
|
|
|
|
|
|
@qr_code_bp.route('/')
|
|
@login_required
|
|
@recht_vereist('qr_codes')
|
|
def qr_generator():
|
|
return render_template('qr_code.html', db_toegang=heeft_recht('toestellen_beheren'), db_velden=DB_VELDEN,
|
|
qr_velden=QR_VELDEN, statussen=STATUSSEN, max_db_codes=MAX_DB_CODES)
|
|
|
|
@qr_code_bp.route('/download-template')
|
|
@login_required
|
|
@recht_vereist('qr_codes')
|
|
def download_template():
|
|
df = pd.DataFrame(columns=["QR-Code (Link/ID)", "Koptekst", "Voettekst"])
|
|
|
|
buffer = io.BytesIO()
|
|
with pd.ExcelWriter(buffer, engine='openpyxl') as writer:
|
|
df.to_excel(writer, index=False)
|
|
|
|
buffer.seek(0)
|
|
|
|
return send_file(
|
|
buffer,
|
|
as_attachment=True,
|
|
download_name='sjabloon_qr.xlsx',
|
|
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
)
|
|
|
|
@qr_code_bp.route('/upload', methods=['POST'])
|
|
@login_required
|
|
@recht_vereist('qr_codes')
|
|
def upload_file():
|
|
if 'file' not in request.files:
|
|
flash("Geen bestand geselecteerd", "warning")
|
|
return redirect(url_for('qr_generator.qr_generator'))
|
|
|
|
file = request.files['file']
|
|
show_top = request.form.get('show_top') == 'on'
|
|
show_bottom = request.form.get('show_bottom') == 'on'
|
|
|
|
if file and file.filename != '':
|
|
try:
|
|
zip_data = generate_qr_images(file, show_top, show_bottom)
|
|
return send_file(
|
|
zip_data,
|
|
mimetype='application/zip',
|
|
as_attachment=True,
|
|
download_name='qr_codes.zip'
|
|
)
|
|
except Exception as e:
|
|
flash(f"Fout bij verwerken: {str(e)}", "danger")
|
|
return redirect(url_for('qr_generator.qr_generator'))
|
|
|
|
return redirect(url_for('qr_generator.qr_generator'))
|
|
|
|
|
|
@qr_code_bp.route('/database', methods=['POST'])
|
|
@login_required
|
|
@recht_vereist('qr_codes')
|
|
def database_qr():
|
|
"""QR-codes van toestelnummers, serienummers of SPB-nummers uit de database (ZIP met PNG's)."""
|
|
terug = redirect(url_for('qr_generator.qr_generator'))
|
|
if not heeft_recht('toestellen_beheren'):
|
|
flash("Je hebt het recht 'Toestelleninventaris beheren' nodig om QR-codes uit de database te maken.", "error")
|
|
return terug
|
|
qr_veld = request.form.get('qr_veld')
|
|
if qr_veld not in QR_VELDEN:
|
|
flash("Kies wat er in de QR-code moet komen.", "warning")
|
|
return terug
|
|
boven = request.form.get('boven') if request.form.get('boven') in DB_VELDEN else None
|
|
onder = request.form.get('onder') if request.form.get('onder') in DB_VELDEN else None
|
|
nummers = [n.strip() for n in re.split(r'[\s,;]+', request.form.get('nummers') or '') if n.strip()]
|
|
try:
|
|
rijen = toestellen_voor_qr(qr_veld, request.form.get('status') or '', (request.form.get('zoek') or '').strip(),
|
|
nummers)
|
|
except Exception as e:
|
|
flash(f"Fout bij het ophalen van de toestellen: {e}", "danger")
|
|
return terug
|
|
if not rijen:
|
|
flash(f"Geen toestellen gevonden met een {DB_VELDEN[qr_veld][0].split(' (')[0].lower()} voor deze keuze.",
|
|
"warning")
|
|
return terug
|
|
if len(rijen) > MAX_DB_CODES:
|
|
flash(f"Dat zijn {len(rijen)} toestellen; maximaal {MAX_DB_CODES} per keer. Gebruik een filter.", "warning")
|
|
return terug
|
|
|
|
font = _lettertype()
|
|
zip_buffer = io.BytesIO()
|
|
gebruikt = set()
|
|
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
|
|
for r in rijen:
|
|
qr_text = str(r[qr_veld]).strip()
|
|
png = _qr_png(qr_text, str(r[boven] or '') if boven else '', str(r[onder] or '') if onder else '', font)
|
|
zip_file.writestr(_bestandsnaam(qr_text, gebruikt), png)
|
|
zip_buffer.seek(0)
|
|
if nummers:
|
|
gevonden = {str(r[qr_veld]).strip().upper() for r in rijen}
|
|
niet = [n for n in nummers if n.upper() not in gevonden]
|
|
if niet:
|
|
flash(f"Niet gevonden: {', '.join(niet[:20])}{' ...' if len(niet) > 20 else ''}", "warning")
|
|
return send_file(zip_buffer, mimetype='application/zip', as_attachment=True,
|
|
download_name=f"qr_{qr_veld}.zip")
|