PIVOT 1.1.0

This commit is contained in:
marijn committed 2026-10-14 13:34:37 +02:00
commit 8a53234eb6
161 files changed
+31109

No files matched your search

+229
View File
@@ -0,0 +1,229 @@
"""Internaat: lijst van alle internen van de actieve school (Informat Internaat WEB API)."""
import csv
import io
import time
from datetime import date
from flask import Blueprint, render_template, request, Response, redirect, url_for, flash
from blueprints.auth import login_required
from rechten import recht_vereist
from services.facturatie_service import schooljaren
from services.internaat_service import InternaatService, InternaatFout
from tenancy import huidige_school
internaat_bp = Blueprint('internaat', __name__)
def _keuzes():
jaren = schooljaren()
schooljaar = request.args.get('schooljaar') if request.args.get('schooljaar') in jaren else jaren[0]
try:
peildatum = date.fromisoformat(request.args.get('peildatum') or '')
except ValueError:
peildatum = date.today()
return schooljaar, jaren, peildatum
@internaat_bp.route("/internen", methods=["GET"])
@login_required
@recht_vereist('internen')
def internen():
school = huidige_school()
schooljaar, jaren, peildatum = _keuzes()
rijen, fout, opgehaald = [], None, None
if not school.heeft('INFORMAT_ID', 'INFORMAT_SECRET'):
fout = "Informat is nog niet ingesteld voor deze school (🔌 Koppelingen › Informat)."
else:
try:
rijen, t = InternaatService().internen(schooljaar, peildatum, vernieuw=bool(request.args.get('vernieuw')))
opgehaald = time.strftime('%d-%m-%Y %H:%M', time.localtime(t))
except InternaatFout as e:
fout = str(e)
except Exception as e:
fout = f"Internen ophalen mislukt: {e}"
internaten = sorted({r['internaat'] for r in rijen})
return render_template("internen.html", rijen=rijen, fout=fout, opgehaald=opgehaald, schooljaar=schooljaar,
jaren=jaren, peildatum=peildatum, internaten=internaten, school_naam=school.naam)
@internaat_bp.route("/internen/smartschool.json", methods=["POST"])
@login_required
@recht_vereist('internen')
def internen_smartschool():
"""Welke internen bestaan als gebruiker in Smartschool? {pid: gebruikersnaam | null}"""
from flask import jsonify
from services.internaat_service import smartschool_status
personen = (request.get_json(silent=True) or {}).get('personen') or []
try:
return jsonify(ok=True, status=smartschool_status(personen[:2000]))
except InternaatFout as e:
return jsonify(ok=False, fout=str(e))
except Exception as e:
return jsonify(ok=False, fout=f"Smartschool kon niet bevraagd worden: {e}")
@internaat_bp.route("/internen/export.csv", methods=["GET"])
@login_required
@recht_vereist('internen')
def export():
schooljaar, _, peildatum = _keuzes()
try:
rijen, _ = InternaatService().internen(schooljaar, peildatum)
except Exception as e:
flash(str(e), "error")
return redirect(url_for('internaat.internen', schooljaar=schooljaar))
internaat = request.args.get('internaat') or ''
zoek = (request.args.get('zoek') or '').strip().lower()
if internaat:
rijen = [r for r in rijen if r['internaat'] == internaat]
if zoek:
rijen = [r for r in rijen if zoek in ' '.join(str(r[k]) for k in ('naam', 'voornaam', 'stamnummer', 'kamer', 'vestiging', 'gemeente')).lower()]
datum = lambda d: d.strftime('%d/%m/%Y') if d else ''
buf = io.StringIO()
w = csv.writer(buf, delimiter=';')
w.writerow(['Naam', 'Voornaam', 'Geboortedatum', 'Geslacht', 'Stamnummer', 'Internaat', 'Vestiging', 'Kamer',
'Ingeschreven van', 'Ingeschreven tot', 'Financiering', 'Woonplaats', 'Contact 1', 'Gsm 1', 'E-mail 1',
'Contact 2', 'Gsm 2', 'E-mail 2', 'Smartschool'])
ss_cache = huidige_school().cache.get('ss_internen') or {}
def ss(r):
hit = ss_cache.get((str(r['id']), r['voornaam'], r['naam']))
return '' if not hit else (hit[1] or 'niet gevonden')
for r in rijen:
o = (r['ouders'] + [{}, {}])[:2]
w.writerow([r['naam'], r['voornaam'], datum(r['geboortedatum']), r['geslacht'], r['stamnummer'], r['internaat'],
r['vestiging'], r['kamer'], datum(r['begin']), datum(r['einde']), r['financiering'], r['gemeente']]
+ sum(([f"{x.get('naam', '')} ({x['type']})" if x.get('type') else x.get('naam', ''), x.get('gsm', ''),
x.get('email', '')] for x in o), []) + [ss(r)])
return Response('' + buf.getvalue(), mimetype='text/csv; charset=utf-8', headers={
'Content-Disposition': f"attachment; filename=Internen_{huidige_school().code}_{peildatum:%Y%m%d}.csv"})
# ---------------------------------------------------------------------
# Aanwezigheden (register per week: ochtend en avond)
# ---------------------------------------------------------------------
from datetime import timedelta
from flask import jsonify, session
from services import aanwezigheid_service as aw
def _week():
try:
d = date.fromisoformat(request.args.get('week') or '')
except ValueError:
d = date.today()
return aw.maandag_van(d)
def _internen_van_week(maandag):
"""Internen die in de week ingeschreven zijn (peildatum: vandaag als die in de week valt, anders woensdag)."""
vandaag = date.today()
peildatum = vandaag if maandag <= vandaag <= maandag + timedelta(days=6) else maandag + timedelta(days=2)
rijen, _ = InternaatService().internen(aw.schooljaar_van(peildatum), peildatum)
meerdere = len({r['internaat'] for r in rijen}) > 1
for r in rijen:
r['groep'] = aw.groep_van(r, meerdere)
r['pid'] = str(r['id'])
return rijen
@internaat_bp.route("/aanwezigheden", methods=["GET"])
@login_required
@recht_vereist('internaat_aanwezigheid')
def aanwezigheden():
school = huidige_school()
maandag = _week()
rijen, fout, data = [], None, {}
try:
rijen = _internen_van_week(maandag)
data = aw.week(maandag)
except InternaatFout as e:
fout = str(e)
except Exception as e:
fout = f"Internen ophalen mislukt: {e}"
groepen = {}
for r in sorted(rijen, key=lambda r: (r['groep'].lower(), r['naam'].lower(), r['voornaam'].lower())):
groepen.setdefault(r['groep'], []).append(r)
dagen = [maandag + timedelta(days=i) for i in range(7)]
return render_template("aanwezigheden.html", groepen=groepen, data=data, dagen=dagen, maandag=maandag,
fout=fout, school_naam=school.naam, vandaag=date.today(), dagnamen=aw.DAGEN,
dagnamen_voluit=aw.DAGEN_VOLUIT, aantal=len(rijen),
vorige=maandag - timedelta(days=7), volgende=maandag + timedelta(days=7),
weeknr=maandag.isocalendar()[1])
@internaat_bp.route("/aanwezigheden/zet", methods=["POST"])
@login_required
@recht_vereist('internaat_aanwezigheid')
def aanwezigheid_zet():
d = request.get_json(silent=True) or {}
try:
datum = date.fromisoformat(str(d.get('datum') or ''))
aw.zet(datum, str(d.get('persoon') or '')[:64], d.get('moment'), d.get('status'), d.get('naam'),
session.get('naam') or session.get('user'))
except ValueError as e:
return jsonify(ok=False, fout=str(e) or 'Ongeldige gegevens.'), 400
except Exception as e:
return jsonify(ok=False, fout=f"Bewaren mislukt: {e}"), 500
return jsonify(ok=True)
@internaat_bp.route("/aanwezigheden/export.xlsx", methods=["GET"])
@login_required
@recht_vereist('internaat_aanwezigheid')
def aanwezigheden_export():
"""Excel van de week, met dezelfde filters als op het scherm (?groep=&zoek=)."""
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
maandag = _week()
try:
rijen = _internen_van_week(maandag)
data = aw.week(maandag)
except Exception as e:
flash(str(e), "error")
return redirect(url_for('internaat.aanwezigheden', week=maandag.isoformat()))
groep, zoek = request.args.get('groep') or '', (request.args.get('zoek') or '').strip().lower()
rijen = [r for r in rijen if (not groep or r['groep'] == groep)
and (not zoek or zoek in f"{r['naam']} {r['voornaam']}".lower())]
rijen.sort(key=lambda r: (r['groep'].lower(), r['naam'].lower(), r['voornaam'].lower()))
labels = {'aanwezig': 'Aanwezig', 'afwezig': 'Afwezig'}
kleur = {'aanwezig': PatternFill('solid', fgColor='D7EDDF'), 'afwezig': PatternFill('solid', fgColor='F6D5CF')}
wb = Workbook()
ws = wb.active
ws.title = f"Week {maandag.isocalendar()[1]}"
kop = ['Naam', 'Voornaam', 'Groep', 'Kamer']
for i, dag in enumerate(aw.DAGEN_VOLUIT):
d = maandag + timedelta(days=i)
kop += [f"{dag} {d:%d/%m} ochtend", f"{dag} {d:%d/%m} avond"]
ws.append(kop)
for c in ws[1]:
c.font = Font(bold=True)
c.alignment = Alignment(wrap_text=True, vertical='top')
vorige_groep = None
for r in rijen:
if r['groep'] != vorige_groep:
ws.append([f"Groep: {r['groep']}"])
ws.cell(ws.max_row, 1).font = Font(bold=True)
vorige_groep = r['groep']
rij = [r['naam'], r['voornaam'], r['groep'], r['kamer']]
for i in range(7):
for m in aw.MOMENTEN:
rij.append(labels.get(data.get(f"{r['pid']}:{i}:{m}"), ''))
ws.append(rij)
for c in ws[ws.max_row][4:]:
for k, f in kleur.items():
if c.value == labels[k]:
c.fill = f
ws.column_dimensions['A'].width = 22
ws.column_dimensions['B'].width = 16
ws.column_dimensions['C'].width = 16
ws.column_dimensions['D'].width = 22
from openpyxl.utils import get_column_letter
for i in range(5, 5 + 14):
ws.column_dimensions[get_column_letter(i)].width = 13
ws.freeze_panes = 'C2'
buf = io.BytesIO()
wb.save(buf)
return Response(buf.getvalue(), mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={'Content-Disposition': f"attachment; filename=Aanwezigheden_{huidige_school().code}_"
f"{maandag:%Y}-W{maandag.isocalendar()[1]:02d}.xlsx"})