Author SHA1 Message Date
Kristof 2b4e8de681 qr-codes genereren op basis van database tables 2026-10-14 15:13:40 +02:00
2 changed files with 184 additions and 27 deletions

No files matched your search

+129 -26
View File
@@ -1,42 +1,31 @@
import io import io
import re
import zipfile import zipfile
import qrcode import qrcode
import pandas as pd import pandas as pd
from flask import Blueprint, render_template, request, send_file, flash, redirect, url_for from flask import Blueprint, render_template, request, send_file, flash, redirect, url_for
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from blueprints.auth import login_required 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 # definieren blueprint
qr_code_bp = Blueprint('qr_generator', __name__, template_folder='templates') qr_code_bp = Blueprint('qr_generator', __name__, template_folder='templates')
def generate_qr_images(excel_file, show_top, show_bottom): def _lettertype():
# Overslaan van de header rij
df = pd.read_excel(excel_file, header=0)
zip_buffer = io.BytesIO()
try: try:
# Try loading your local or system Arial font # Try loading your local or system Arial font
font = ImageFont.truetype("arial.ttf", 20) return ImageFont.truetype("arial.ttf", 20)
except IOError: except IOError:
try: try:
return ImageFont.load_default_font(size=20)
font = ImageFont.load_default_font(size=20)
except AttributeError: except AttributeError:
# Absolute fallback if Pillow is outdated, but textbbox WILL fail if this hits # Absolute fallback if Pillow is outdated, but textbbox WILL fail if this hits
font = ImageFont.load_default() return ImageFont.load_default()
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
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 = qrcode.QRCode(error_correction=qrcode.constants.ERROR_CORRECT_H)
qr.add_data(qr_text) qr.add_data(qr_text)
qr.make(fit=True) qr.make(fit=True)
@@ -53,30 +42,96 @@ def generate_qr_images(excel_file, show_top, show_bottom):
qr_y = padding qr_y = padding
canvas.paste(qr_img, (qr_x, qr_y)) canvas.paste(qr_img, (qr_x, qr_y))
if show_top and top_text: if top_text:
bbox = draw.textbbox((0, 0), top_text, font=font) bbox = draw.textbbox((0, 0), top_text, font=font)
text_w = bbox[2] - bbox[0] text_w = bbox[2] - bbox[0]
draw.text(((canvas.width - text_w) / 2, 5), top_text, fill="black", font=font) draw.text(((canvas.width - text_w) / 2, 5), top_text, fill="black", font=font)
if show_bottom and bottom_text: if bottom_text:
bbox = draw.textbbox((0, 0), bottom_text, font=font) bbox = draw.textbbox((0, 0), bottom_text, font=font)
text_w = bbox[2] - bbox[0] text_w = bbox[2] - bbox[0]
draw.text(((canvas.width - text_w) / 2, qr_y + qr_height + 5), bottom_text, fill="black", font=font) draw.text(((canvas.width - text_w) / 2, qr_y + qr_height + 5), bottom_text, fill="black", font=font)
img_io = io.BytesIO() img_io = io.BytesIO()
canvas.save(img_io, 'PNG') canvas.save(img_io, 'PNG')
img_io.seek(0) return img_io.getvalue()
zip_file.writestr(f"qr_{index+1}.png", 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) zip_buffer.seek(0)
return zip_buffer 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('/') @qr_code_bp.route('/')
@login_required @login_required
@recht_vereist('qr_codes') @recht_vereist('qr_codes')
def qr_generator(): def qr_generator():
return render_template('qr_code.html', db_toegang=heeft_recht('toestellen_beheren'), db_velden=DB_VELDEN,
return render_template('qr_code.html') qr_velden=QR_VELDEN, statussen=STATUSSEN, max_db_codes=MAX_DB_CODES)
@qr_code_bp.route('/download-template') @qr_code_bp.route('/download-template')
@login_required @login_required
@@ -123,3 +178,51 @@ def upload_file():
return redirect(url_for('qr_generator.qr_generator')) return redirect(url_for('qr_generator.qr_generator'))
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")
+55 -1
View File
@@ -26,7 +26,7 @@
<div class="page-head"> <div class="page-head">
<div> <div>
<h2>💡 QR-codes maken</h2> <h2>💡 QR-codes maken</h2>
<p class="muted" style="margin:0">Maak eenvoudig QR-codes aan vanuit een Excel-bestand.</p> <p class="muted" style="margin:0">Maak eenvoudig QR-codes aan vanuit een Excel-bestand{{ ' of uit de database (hieronder)' if db_toegang }}.</p>
</div> </div>
</div> </div>
@@ -50,4 +50,58 @@
</li> </li>
</ol> </ol>
</div> </div>
{% if db_toegang %}
<div class="card">
<div class="page-head">
<div>
<h2>🗄️ QR-codes uit de database</h2>
<p class="muted" style="margin:0">Maak QR-codes van de toestellen in PIVOT: één PNG per toestel, samen in een ZIP.</p>
</div>
</div>
<form action="{{ url_for('qr_generator.database_qr') }}" method="post">
<div class="field">
<label for="qr_veld">In de QR-code</label>
<select id="qr_veld" name="qr_veld" required>
{% for v in qr_velden %}<option value="{{ v }}">{{ db_velden[v][0] }}</option>{% endfor %}
</select>
<div class="field-hint">Toestellen zonder deze waarde worden overgeslagen.</div>
</div>
<div class="options-box">
<div class="field">
<label for="boven">Tekst boven de QR-code</label>
<select id="boven" name="boven">
<option value="">Geen</option>
{% for k, v in db_velden.items() %}<option value="{{ k }}">{{ v[0] }}</option>{% endfor %}
</select>
</div>
<div class="field">
<label for="onder">Tekst onder de QR-code</label>
<select id="onder" name="onder">
<option value="">Geen</option>
{% for k, v in db_velden.items() %}<option value="{{ k }}" {{ 'selected' if k == 'toestelnummer' }}>{{ v[0] }}</option>{% endfor %}
</select>
</div>
</div>
<div class="field">
<label for="status">Status</label>
<select id="status" name="status">
<option value="">Alle toestellen</option>
{% for s in statussen %}<option value="{{ s }}">{{ s }}</option>{% endfor %}
</select>
</div>
<div class="field">
<label for="zoek">Zoeken (optioneel)</label>
<input type="text" id="zoek" name="zoek" placeholder="bv. CB-01 of Lenovo">
<div class="field-hint">Enkel toestellen waarvan het toestelnummer, serienummer, SPB-nummer of type deze tekst bevat.</div>
</div>
<div class="field">
<label for="nummers">Enkel deze nummers (optioneel)</label>
<textarea id="nummers" name="nummers" rows="4" placeholder="Eén per regel, of gescheiden door komma's"></textarea>
<div class="field-hint">Nummers van het gekozen soort (zie "In de QR-code"). Leeg = alle toestellen. Max. {{ max_db_codes }} per keer.</div>
</div>
<button type="submit" class="btn-block btn-lg">🚀 Genereer QR-codes (ZIP)</button>
</form>
</div>
{% endif %}
{% endblock %} {% endblock %}