From 2b4e8de68126cc69e1fec748f52e167cc81141a8 Mon Sep 17 00:00:00 2001 From: Kristof Date: Wed, 14 Oct 2026 15:13:40 +0200 Subject: [PATCH] qr-codes genereren op basis van database tables --- blueprints/qr_code_bp.py | 193 ++++++++++++++++++++++++++++++--------- templates/qr_code.html | 56 +++++++++++- 2 files changed, 203 insertions(+), 46 deletions(-) diff --git a/blueprints/qr_code_bp.py b/blueprints/qr_code_bp.py index 5e97b1e..aef193a 100644 --- a/blueprints/qr_code_bp.py +++ b/blueprints/qr_code_bp.py @@ -1,31 +1,68 @@ 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 rechten import recht_vereist # Zorg dat dit pad klopt naar je auth_utils.py +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 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() - +def _lettertype(): try: # Try loading your local or system Arial font - font = ImageFont.truetype("arial.ttf", 20) + return ImageFont.truetype("arial.ttf", 20) except IOError: try: - - font = ImageFont.load_default_font(size=20) + return ImageFont.load_default_font(size=20) except AttributeError: # Absolute fallback if Pillow is outdated, but textbbox WILL fail if this hits - font = ImageFont.load_default() + 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(): @@ -33,50 +70,68 @@ def generate_qr_images(excel_file, show_top, show_bottom): 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 - - 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 show_top and 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 show_bottom and 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') - img_io.seek(0) - zip_file.writestr(f"qr_{index+1}.png", img_io.getvalue()) + 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') + 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 @@ -122,4 +177,52 @@ def upload_file(): flash(f"Fout bij verwerken: {str(e)}", "danger") return redirect(url_for('qr_generator.qr_generator')) - return redirect(url_for('qr_generator.qr_generator')) \ No newline at end of file + 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") diff --git a/templates/qr_code.html b/templates/qr_code.html index 79203f6..eac0207 100644 --- a/templates/qr_code.html +++ b/templates/qr_code.html @@ -26,7 +26,7 @@

💡 QR-codes maken

-

Maak eenvoudig QR-codes aan vanuit een Excel-bestand.

+

Maak eenvoudig QR-codes aan vanuit een Excel-bestand{{ ' of uit de database (hieronder)' if db_toegang }}.

@@ -50,4 +50,58 @@ + +{% if db_toegang %} +
+
+
+

🗄️ QR-codes uit de database

+

Maak QR-codes van de toestellen in PIVOT: één PNG per toestel, samen in een ZIP.

+
+
+
+
+ + +
Toestellen zonder deze waarde worden overgeslagen.
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
Enkel toestellen waarvan het toestelnummer, serienummer, SPB-nummer of type deze tekst bevat.
+
+
+ + +
Nummers van het gekozen soort (zie "In de QR-code"). Leeg = alle toestellen. Max. {{ max_db_codes }} per keer.
+
+ +
+
+{% endif %} {% endblock %} -- 2.54.0