roosterwijs/backend/modules/uren/views.py
bes-r c845588e5e
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
Add roles modules audit logging and CI
2026-07-04 23:01:06 +02:00

189 lines
7.1 KiB
Python

"""Urenoverzicht: berekent lesuren per groep/vak en per leerling.
Roosterblokken zijn wekelijks: per schooljaar telt een blok zo vaak mee als er
lesdagen op die weekdag zijn (vrije dagen/vakanties/studiedagen tellen niet mee),
minus de keren dat het blok via een uitzondering vervalt. Een blok met een
geldigheidsperiode (geldig_van/geldig_tot) telt alleen binnen dat bereik mee.
"""
from datetime import timedelta
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from core.audit import audit_log
from core.models import KalenderDag, Persoon, Roosterblok, Rol, Schooljaar, Subgroep, Tijdslot
from core.permissions import StaffDataPermission, module_permission_class
from .models import UrenMomentopname
UrenModuleEnabled = module_permission_class("uren")
def _duur_uren(ts):
s, e = ts.start_tijd, ts.eind_tijd
return ((e.hour * 60 + e.minute) - (s.hour * 60 + s.minute)) / 60.0
def bereken_uren(sj):
"""Bereken het volledige urenoverzicht voor een schooljaar (dict)."""
vrije = set(KalenderDag.objects.filter(schooljaar=sj).values_list("datum", flat=True))
lesdagen = {0: 0, 1: 0, 2: 0, 3: 0, 4: 0}
d = sj.start_datum
while d <= sj.eind_datum:
wd = d.weekday()
if wd <= 4 and d not in vrije:
lesdagen[wd] += 1
d += timedelta(days=1)
blokken = list(
Roosterblok.objects.filter(schooljaar=sj, sjabloon=False)
.select_related("tijdslot", "activiteit", "groep", "subgroep", "leerling", "leerling__groep")
.prefetch_related("uitzonderingen")
)
subgroepen = {s.id: s for s in Subgroep.objects.prefetch_related("leden").all()}
sub_leden = {sid: set(s.leden.values_list("id", flat=True)) for sid, s in subgroepen.items()}
leerlingen = list(Persoon.objects.filter(rol=Rol.LEERLING).select_related("groep"))
def _tel_weekdag(van, tot, wd):
n = 0
d2 = van
while d2 <= tot:
if d2.weekday() == wd and d2 not in vrije:
n += 1
d2 += timedelta(days=1)
return n
def occurrences(blok):
wd = blok.tijdslot.dag
van = max(blok.geldig_van or sj.start_datum, sj.start_datum)
tot = min(blok.geldig_tot or sj.eind_datum, sj.eind_datum)
if van > tot:
return 0
if blok.geldig_van is None and blok.geldig_tot is None:
basis = lesdagen.get(wd, 0)
else:
basis = _tel_weekdag(van, tot, wd)
weg = 0
for u in blok.uitzonderingen.all():
if u.vervalt and van <= u.datum <= tot and u.datum.weekday() == wd and u.datum not in vrije:
weg += 1
return max(basis - weg, 0)
def groep_van_blok(b):
if b.groep_id:
return b.groep
if b.subgroep_id:
s = subgroepen.get(b.subgroep_id)
return s.groep if s else None
if b.leerling_id:
return b.leerling.groep
return None
blok_uren = {}
groep_map = {}
for b in blokken:
u = _duur_uren(b.tijdslot) * occurrences(b)
blok_uren[b.id] = u
g = groep_van_blok(b)
if g:
gm = groep_map.setdefault(g.id, {"naam": g.naam, "vakken": {}})
gm["vakken"][b.activiteit.naam] = gm["vakken"].get(b.activiteit.naam, 0) + u
per_groep = []
for gm in groep_map.values():
vakken = [{"vak": k, "uren": round(v, 1)} for k, v in sorted(gm["vakken"].items())]
per_groep.append({
"groep": gm["naam"],
"totaal": round(sum(x["uren"] for x in vakken), 1),
"vakken": vakken,
})
per_groep.sort(key=lambda x: x["groep"])
stages = []
alle_ts = []
try:
from modules.stage.models import Stage
alle_ts = list(Tijdslot.objects.all())
stages = list(Stage.objects.filter(actief=True))
except Exception:
pass
per_leerling = []
for l in leerlingen:
vakken = {}
for b in blokken:
hoort = (
(b.groep_id and l.groep_id == b.groep_id)
or (b.subgroep_id and l.id in sub_leden.get(b.subgroep_id, set()))
or (b.leerling_id == l.id)
)
if hoort:
vakken[b.activiteit.naam] = vakken.get(b.activiteit.naam, 0) + blok_uren[b.id]
stage_uren = 0
for st in stages:
if st.leerling_id != l.id:
continue
for ts in alle_ts:
if st.dekt_tijdslot(ts):
stage_uren += _duur_uren(ts) * lesdagen.get(ts.dag, 0)
vak_list = [{"vak": k, "uren": round(v, 1)} for k, v in sorted(vakken.items())]
totaal = sum(x["uren"] for x in vak_list) + stage_uren
if vak_list or stage_uren:
per_leerling.append({
"leerling": l.volledige_naam,
"groep": l.groep.naam if l.groep else None,
"vakken": vak_list,
"stage_uren": round(stage_uren, 1),
"totaal": round(totaal, 1),
})
per_leerling.sort(key=lambda x: x["leerling"])
return {
"schooljaar": sj.naam,
"lesdagen": lesdagen,
"per_groep": per_groep,
"per_leerling": per_leerling,
}
@api_view(["GET"])
@permission_classes([StaffDataPermission, UrenModuleEnabled])
def urenoverzicht(request):
sjid = request.query_params.get("schooljaar")
if not sjid:
return Response({"detail": "Geef een schooljaar op."}, status=400)
try:
sj = Schooljaar.objects.get(pk=sjid)
except Schooljaar.DoesNotExist:
return Response({"detail": "Schooljaar niet gevonden."}, status=404)
return Response(bereken_uren(sj))
@api_view(["GET", "POST"])
@permission_classes([StaffDataPermission, UrenModuleEnabled])
def momentopnames(request):
"""GET: lijst van momentopnames (samenvatting). POST: bewaar huidige stand."""
sjid = request.query_params.get("schooljaar") or request.data.get("schooljaar")
if not sjid:
return Response({"detail": "Geef een schooljaar op."}, status=400)
try:
sj = Schooljaar.objects.get(pk=sjid)
except Schooljaar.DoesNotExist:
return Response({"detail": "Schooljaar niet gevonden."}, status=404)
if request.method == "POST":
payload = bereken_uren(sj)
opname = UrenMomentopname.objects.create(schooljaar=sj, payload=payload)
audit_log(request, "create", f"UrenMomentopname:{opname.pk}", {"schooljaar": sj.pk})
return Response({"id": opname.id, "gemaakt_op": opname.gemaakt_op}, status=201)
opnames = UrenMomentopname.objects.filter(schooljaar=sj)
# Bouw een leerling x momentopname-tabel met totaal-uren.
kolommen = []
leerling_rijen = {}
for o in opnames:
kolommen.append({"id": o.id, "gemaakt_op": o.gemaakt_op})
for l in (o.payload or {}).get("per_leerling", []):
leerling_rijen.setdefault(l["leerling"], {})[o.id] = l["totaal"]
rijen = [{"leerling": naam, "totalen": tot} for naam, tot in sorted(leerling_rijen.items())]
return Response({"schooljaar": sj.naam, "momentopnames": kolommen, "leerlingen": rijen})