Initial commit

This commit is contained in:
Kristof committed 2026-10-14 12:29:29 +02:00
commit 536ff43ce4
158 files changed
+30355

No files matched your search

+549
View File
@@ -0,0 +1,549 @@
import json
import requests
from datetime import datetime, timezone # Essentieel voor de timestamp
from dateutil.relativedelta import relativedelta
from config import Config
from database import execute_query
# Informat geeft de volledige leerlingenlijst soms pas na een minuut of langer terug.
# (verbinden, lezen) in seconden
INFORMAT_TIMEOUT = (15, 300)
ONEROSTER_TIMEOUT = (15, 120)
def informat_get(url, headers, pogingen=2):
"""GET naar Informat met ruime wachttijd; bij een time-out nog één keer opnieuw."""
for poging in range(1, pogingen + 1):
try:
return requests.get(url, headers=headers, timeout=INFORMAT_TIMEOUT)
except requests.exceptions.Timeout:
if poging == pogingen:
raise RuntimeError(
f"Informat antwoordde niet binnen {INFORMAT_TIMEOUT[1] // 60} minuten "
f"({pogingen} pogingen). Probeer het later opnieuw; Informat is soms traag of in onderhoud.")
print(f"[Informat] time-out, nieuwe poging ({poging + 1}/{pogingen})...")
def zorg_vertrokken_kolom():
"""Kolom Leerlingen.Vertrokken_op toevoegen in een bestaande schooldatabase (één keer per school)."""
try:
from tenancy import huidige_school
school = huidige_school()
except Exception:
school = None
if school is not None and school.cache.get('lln_vertrokken_kolom'):
return
kolommen = {r['Field'].lower() for r in execute_query("SHOW COLUMNS FROM Leerlingen") or []}
if 'vertrokken_op' not in kolommen:
execute_query("ALTER TABLE Leerlingen ADD COLUMN Vertrokken_op DATE NULL AFTER Schooljaar_ID", commit=True)
if school is not None:
school.cache['lln_vertrokken_kolom'] = True
def veld_waarden(obj, pad):
"""Alle waarden op een pad met punten, ook door lijsten heen: veld_waarden(st, 'inschrijvingen.vestiging')."""
huidig = [obj]
for deel in pad.split('.'):
volgende = []
for o in huidig:
items = o if isinstance(o, list) else [o]
for it in items:
if isinstance(it, dict) and deel in it:
volgende.append(it[deel])
huidig = volgende
uit = []
for h in huidig:
uit.extend(h if isinstance(h, list) else [h])
return uit
class SyncService:
def __init__(self):
self.conf = Config()
# Informat-gegevens van de huidige school (beheerd via /scholen).
# Een school kan meerdere Informat-instellingsnummers hebben (één per campus).
self.campussen = Config.INFORMAT_CAMPUSSEN or []
self.institute_no = Config.INFORMAT_INST_NUMBER
self.auth_url = Config.INFORMAT_AUTH_URL
self.api_base = Config.INFORMAT_API_BASE
# De scope MOET het instellingsnummer bevatten
self.scope = Config.INFORMAT_FULL_SCOPE
def get_informat_token(self, nummer=None):
"""Haalt een token op met de scope van het (eerste of opgegeven) instellingsnummer."""
from tenancy import informat_scope
scope = informat_scope(Config.INFORMAT_SCOPE, nummer) if nummer else self.scope
payload = {
'grant_type': 'client_credentials',
'client_id': self.conf.INFORMAT_ID,
'client_secret': self.conf.INFORMAT_SECRET,
'scope': scope
}
try:
response = requests.post(self.auth_url, data=payload, timeout=10)
if response.status_code == 200:
return response.json().get('access_token')
except Exception:
return None
return None
def tel_leerlingen(self, refdate, schooljaar, nummer=None):
"""Aantal leerlingen in Informat op `refdate` (YYYY-MM-DD) voor `schooljaar` (bv. 2026-27), voor één
instellingsnummer (standaard het eerste). Geeft (True, aantal) of (False, foutmelding)."""
nummer = nummer or self.institute_no
token = self.get_informat_token(nummer)
if not token:
return False, f"Authenticatie bij Informat mislukt (instellingsnummer {nummer})."
url = f"{self.api_base}/students/?schoolYear={schooljaar}&refdate={refdate}"
headers = {
'Authorization': f'BEARER {token}',
'InstituteNo': nummer,
'Timestamp': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z'), # verplicht bij Informat
'Api-Version': '1',
'Accept': 'application/json'
}
try:
response = informat_get(url, headers)
except Exception as e:
return False, f"Fout tijdens tellen: {e}"
if response.status_code != 200:
return False, f"API fout {response.status_code} (instellingsnummer {nummer}): {response.text[:300]}"
data = response.json()
lijst = data if isinstance(data, list) else data.get('students', [])
self._laatste_lijst = lijst # voor tellen per campusveld en de veldentest
return True, len(lijst)
def informat_lijst(self, refdate, schooljaar, nummer=None):
"""De volledige leerlingenlijst van Informat (zelfde oproep als het tellen). (True, lijst) of (False, fout)."""
ok, res = self.tel_leerlingen(refdate, schooljaar, nummer)
return (True, self._laatste_lijst) if ok else (False, res)
def tel_locatie(self, refdate, schooljaar, locatie_id):
"""Aantal leerlingen op één schoollocatie (Discimus-nummer) via de Informat Student API
(GET /smartschool-users). Geeft (True, aantal) of (False, foutmelding).
Let op: deze API telt enkel leerlingen met een actieve inschrijving (gerealiseerd of parallel, niet GON)
op een locatie die aan het Smartschool-platform gekoppeld is."""
platform = (Config.SS_API_PLATFORM or '').strip()
if not platform:
return False, ("Voor het tellen per campus moet het Smartschool-platform ingevuld zijn "
"(🔌 Koppelingen › Smartschool).")
token = self.get_informat_token(self.institute_no)
if not token:
return False, f"Authenticatie bij Informat mislukt (instellingsnummer {self.institute_no})."
url = f"{str(Config.INFORMAT_STUDENT_API).rstrip('/')}/smartschool-users"
headers = {'Authorization': f'Bearer {token}', 'instituteNo': self.institute_no, 'Accept': 'application/json'}
# Informat vergelijkt het platformadres letterlijk met wat er in Informat staat. Dat kan met of zonder
# https:// of een / op het einde opgeslagen zijn: probeer de varianten tot er één gekend is (en onthoud die).
varianten = list(dict.fromkeys([v for v in (getattr(self, '_ss_platform_url', None), f"https://{platform}",
platform, f"https://{platform}/", f"http://{platform}") if v]))
response, geprobeerd = None, []
for platform_url in varianten:
params = {'smartschoolPlatformUrl': platform_url, 'schoolYear': schooljaar,
'locationIdDiscimus': int(locatie_id) if str(locatie_id).isdigit() else locatie_id,
'referenceDate': refdate}
try:
for poging in (1, 2):
try:
response = requests.get(url, params=params, headers=headers, timeout=INFORMAT_TIMEOUT)
break
except requests.exceptions.Timeout:
if poging == 2:
raise RuntimeError("Informat antwoordde niet op tijd.")
except Exception as e:
return False, f"Fout tijdens tellen: {e}"
geprobeerd.append(platform_url)
if response.status_code == 409 and 'smartschoolplatform' in response.text.lower():
continue # platform onder dit adres niet gekend: volgende variant
self._ss_platform_url = platform_url
break
if response.status_code != 200:
detail = ''
try:
detail = response.json().get('detail') or ''
except ValueError:
detail = response.text[:200]
if response.status_code == 409 and 'smartschoolplatform' in response.text.lower():
return False, (f"Informat kent het Smartschool-platform niet voor instellingsnummer {self.institute_no} "
f"en schooljaar {schooljaar} (geprobeerd: {', '.join(geprobeerd)}). Is in Informat "
"voor dit instellingsnummer en dit schooljaar een Smartschool-koppeling actief?")
return False, f"Student API fout {response.status_code} (campus {locatie_id}): {detail}"
data = response.json()
lijst = data if isinstance(data, list) else data.get('items', [])
# Eén leerling kan meer dan één inschrijving hebben: uniek op persoon
return True, len({u['ppersoon'] if u.get('ppersoon') is not None else (u.get('persoonId') or id(u))
for u in lijst})
def tel_per_campus(self, refdate, schooljaar):
"""Geeft (True, totaal, detail) of (False, fout, None). detail = [{'naam', 'nummer', 'aantal'}] als de
school meerdere campussen heeft, anders None.
- Campussen (Discimus-nummers) ingesteld → per locatie geteld via de Student API.
- Anders: per instellingsnummer (meestal één) via de leerlingen-API zoals voorheen."""
locaties = Config.INFORMAT_LOCATIES or []
veld = (Config.INFORMAT_CAMPUS_VELD or '').strip()
if veld:
return self._tel_op_veld(refdate, schooljaar, veld, dict(locaties))
if locaties:
telling = [(loc, naam, lambda loc=loc: self.tel_locatie(refdate, schooljaar, loc)) for loc, naam in locaties]
else:
campussen = self.campussen or [(self.institute_no, '')]
telling = [(nr, naam, lambda nr=nr: self.tel_leerlingen(refdate, schooljaar, nr)) for nr, naam in campussen]
detail, totaal = [], 0
for nummer, naam, tel in telling:
ok, res = tel()
if not ok:
return False, (f"{naam}: {res}" if len(telling) > 1 else res), None
detail.append({'naam': naam or nummer, 'nummer': nummer, 'aantal': res})
totaal += res
return True, totaal, (detail if len(detail) > 1 else None)
def _tel_op_veld(self, refdate, schooljaar, veld, namen):
"""Haalt de leerlingenlijst één keer op en telt per waarde van `veld` (pad met punten, bv.
'inschrijvingen.vestiging'). `namen` vertaalt een waarde naar een campusnaam (uit 'Campussen')."""
lijst, gezien = [], set()
for nummer, naam in (self.campussen or [(self.institute_no, '')]): # alle instellingsnummers van de school
ok, deel = self.informat_lijst(refdate, schooljaar, nummer)
if not ok:
return False, (f"{naam}: {deel}" if len(self.campussen) > 1 else deel), None
for st in deel:
sleutel = str(st.get('pPersoon') or st.get('id') or id(st))
if sleutel not in gezien:
gezien.add(sleutel)
lijst.append(st)
per = {}
for st in lijst:
for w in set(str(v) for v in veld_waarden(st, veld) if v not in (None, '')) or {'?'}:
per[w] = per.get(w, 0) + 1
volgorde = [w for w in namen if w in per] + sorted(w for w in per if w not in namen)
detail = [{'naam': namen.get(w) or (f"Onbekend ({veld} leeg)" if w == '?' else f"Campus {w}"),
'nummer': '' if w == '?' else w, 'aantal': per[w]} for w in volgorde]
return True, len(lijst), (detail if len(detail) > 1 else None)
def sync_student_count(self, school_naam=None):
"""Huidig aantal (peildatum vandaag + 1 maand, zodat net ingeschreven leerlingen meetellen)."""
refdate = (datetime.now() + relativedelta(months=1)).strftime('%Y-%m-%d')
ok, res, detail = self.tel_per_campus(refdate, Config.SCHOOLYEAR)
if not ok:
return False, res
# Bewaren bij de school in de centrale database (overzicht scholengroep)
from database import central_query
central_query("UPDATE Scholen SET Leerlingenaantal = %s, Leerlingenaantal_detail = %s, "
"Leerlingenaantal_sync = NOW() WHERE Code = %s",
(res, json.dumps(detail) if detail else None, Config.SCHOOLNAME_SHORT), commit=True)
if detail:
return True, f"Synchronisatie voltooid: {res} leerlingen (" + ", ".join(
f"{d['naam']}: {d['aantal']}" for d in detail) + ")."
return True, f"Synchronisatie voltooid: {res} leerlingen."
def tel_op_peildatum(self, peildatum, door=None):
"""Telt de leerlingen op een gekozen datum en bewaart het resultaat in Leerlingenaantal_tellingen."""
from database import central_query
from tenancy import huidige_school, _automatisch_schooljaar
schooljaar = _automatisch_schooljaar(peildatum)
ok, res, detail = self.tel_per_campus(peildatum.strftime('%Y-%m-%d'), schooljaar)
if not ok:
return False, res
central_query(
"INSERT INTO Leerlingenaantal_tellingen (School_ID, Peildatum, Schooljaar, Aantal, Detail, Opgevraagd_op, Door) "
"VALUES (%s, %s, %s, %s, %s, NOW(), %s) "
"ON DUPLICATE KEY UPDATE Schooljaar = VALUES(Schooljaar), Aantal = VALUES(Aantal), Detail = VALUES(Detail), "
"Opgevraagd_op = NOW(), Door = VALUES(Door)",
(huidige_school().id, peildatum, schooljaar, res, json.dumps(detail) if detail else None, door),
commit=True)
return True, res
#Haal ss token op
def get_smartschool_token(self):
"""Haalt een OAuth2 token op bij Smartschool."""
resp_ss = requests.post(
self.conf.SS_TOKEN_URL,
data={"grant_type": "client_credentials"},
auth=(self.conf.SS_CLIENT_ID, self.conf.SS_CLIENT_SECRET)
)
return resp_ss.json().get("access_token")
#Vergelijkt leerlingenlijst uit informat met namen uit DB en vult eventuele ontbrekende leerlingen aan met gegevens van Informat (Internnummer) en Smartschool (Gebruikersnaam)
def sync_leerlingen_informat(self):
"""
Synchroniseert leerlingen door Informat data te vergelijken met de DB
en ontbrekende data aan te vullen via Smartschool.
"""
try:
# 1. Haal Informat data op
inf_token = self.get_informat_token()
#Ik zet als parameter voor het ophalen huidige datum +1 maand om leerlingen die net ingeschreven zijn en pas na een vakantie starten mee te kunnen rekenen
future_date = datetime.now() + relativedelta(months=1)
refdate = future_date.strftime('%Y-%m-%d')
inf_url = f"{self.api_base}/students/?schoolYear={self.conf.INFORMAT_SCHOOLYEAR}&refdate={refdate}"
inf_headers = {
'Authorization': f'BEARER {inf_token}',
'InstituteNo': self.institute_no,
'Api-Version': '1',
'Accept': 'application/json'
}
inf_data = requests.get(inf_url, headers=inf_headers).json()
if not inf_data or 'students' not in inf_data:
return False, "Geen data van Informat ontvangen"
# 2. Haal Bestaande Database data op
# We gebruiken execute_query om alle bestaande Internnummers op te halen
db_rows = execute_query("SELECT Internnummer FROM Leerlingen")
db_numbers = {str(row['Internnummer']) for row in db_rows} if db_rows else set()
# 3. Zoek ontbrekende ID's (Informat - Database)
json_numbers = {str(s['pPersoon']) for s in inf_data['students'] if 'pPersoon' in s}
missing_ids = json_numbers - db_numbers
if not missing_ids:
return True, "Iedereen is al gesynchroniseerd."
# 4. Haal Smartschool data op voor de match
ss_token = self.get_smartschool_token()
ss_res = requests.get(
f"{self.conf.SS_BASE_URL}/students",
headers={"Authorization": f"Bearer {ss_token}"}
)
ss_students = ss_res.json().get("users", [])
added_count = 0
for m_id in missing_ids:
found_username = None
for ss_student in ss_students:
ss_internal = str(ss_student.get("metadata", {}).get("smsc.internalNumber", ""))
if ss_internal.strip() == m_id.strip():
found_username = ss_student.get("username")
break
if found_username:
# 5. DATABASE INVOER
execute_query(
"INSERT INTO Leerlingen (Internnummer, Gebruikersnaam, ss_nummer) VALUES (%s, %s, %s)",
(m_id, found_username, "Niet ingesteld"),
commit=True
)
added_count += 1
return True, f"Succes! {added_count} leerlingen toegevoegd."
except Exception as e:
return False, f"Fout tijdens synchronisatie: {str(e)}"
#---------------------Nieuwe aanpak------------------------
import requests
from datetime import datetime, timezone
from config import Config
from database import execute_query
class InformatSmartschoolSync:
def __init__(self):
self.conf = Config()
# Informat-gegevens van de huidige school (beheerd via /scholen)
self.institute_no = Config.INFORMAT_INST_NUMBER
self.auth_url = Config.INFORMAT_AUTH_URL
self.api_base = Config.INFORMAT_API_BASE
# De scope MOET het instellingsnummer bevatten
self.scope = Config.INFORMAT_FULL_SCOPE
self.campussen = Config.INFORMAT_CAMPUSSEN or [(self.institute_no, '')]
def get_informat_token(self, nummer=None):
"""Haalt token op met de scope van het (eerste of opgegeven) instellingsnummer."""
from tenancy import informat_scope
payload = {
'grant_type': 'client_credentials',
'client_id': self.conf.INFORMAT_ID,
'client_secret': self.conf.INFORMAT_SECRET,
'scope': informat_scope(Config.INFORMAT_SCOPE, nummer) if nummer else self.scope
}
try:
response = requests.post(self.auth_url, data=payload, timeout=10)
if response.status_code == 200:
return response.json().get('access_token')
except Exception as e:
print(f"Fout bij ophalen Informat token: {e}")
return None
def get_smartschool_oneroster_students(self):
"""Haalt actieve leerlingen op uit de Smartschool OneRoster API (v1p1). Bij een fout: {}."""
mapping, fout = self.smartschool_koppeling()
if fout:
print(f"Fout bij ophalen Smartschool OneRoster data: {fout}")
return mapping
def smartschool_koppeling(self):
"""{internnummer: {'username', 'ss_nummer'}} uit Smartschool OneRoster. Geeft (mapping, None) of ({}, fout)."""
if not (self.conf.SS_CLIENT_ID and self.conf.SS_CLIENT_SECRET):
return {}, "de OneRoster-koppeling van Smartschool is niet ingesteld"
try:
token_url = self.conf.SS_TOKEN_URL.strip()
if not token_url.startswith(("http://", "https://")):
token_url = f"https://{token_url}"
resp_ss = requests.post(
token_url,
data={"grant_type": "client_credentials"},
auth=(self.conf.SS_CLIENT_ID, self.conf.SS_CLIENT_SECRET),
timeout=10
)
resp_ss.raise_for_status()
ss_token = resp_ss.json().get("access_token")
if not ss_token:
return {}, "geen access_token ontvangen van Smartschool"
headers = {
"Authorization": f"Bearer {ss_token}",
"Accept": "application/json"
}
base_url = self.conf.SS_BASE_URL.strip()
if not base_url.startswith(("http://", "https://")):
base_url = f"https://{base_url}"
base_url = base_url.rstrip('/')
endpoint_url = f"{base_url}/students"
response = requests.get(endpoint_url, headers=headers, timeout=ONEROSTER_TIMEOUT)
response.raise_for_status()
json_data = response.json()
all_students = json_data.get("users", [])
ss_mapping = {}
for student in all_students:
metadata = student.get("metadata", {})
internal_number = metadata.get("smsc.internalNumber")
username = student.get("username")
sourced_id = student.get("sourcedId")
if internal_number:
ss_mapping[str(internal_number).strip()] = {
"username": username,
"ss_nummer": sourced_id if sourced_id else "Niet ingesteld"
}
if not ss_mapping:
return {}, "Smartschool gaf geen enkele leerling met een internnummer terug"
return ss_mapping, None
except Exception as e:
return {}, str(e)
def _informat_leerlingen(self, nummer, school_year, refdate):
"""Leerlingenlijst van één instellingsnummer. Geeft (lijst, None) of (None, fout)."""
inf_token = self.get_informat_token(nummer)
if not inf_token:
return None, f"Informat authenticatie mislukt (instellingsnummer {nummer})."
base_url_inf = self.api_base.strip().rstrip('/')
url = f"{base_url_inf}/students/?schoolYear={school_year}&refdate={refdate}"
headers = {
'Authorization': f'BEARER {inf_token}',
'InstituteNo': nummer,
'Timestamp': datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z'),
'Api-Version': '1',
'Accept': 'application/json'
}
response = informat_get(url, headers)
if response.status_code != 200:
return None, f"Informat API fout {response.status_code} (instellingsnummer {nummer}): {response.text}"
json_data = response.json()
return (json_data if isinstance(json_data, list) else json_data.get('students', json_data.get('items', []))), None
def sync_actieve_leerlingen(self):
"""
Haalt actieve leerlingen op bij Informat (alle campussen / instellingsnummers van de school),
koppelt Smartschool OneRoster info en update de database 'Leerlingen'.
"""
school_year = Config.SCHOOLYEAR
# +1 maand: ook leerlingen die net ingeschreven zijn en pas na een vakantie starten
refdate = (datetime.now() + relativedelta(months=1)).strftime('%Y-%m-%d')
students_list, gezien = [], set()
try:
for nummer, naam in self.campussen:
lijst, fout = self._informat_leerlingen(nummer, school_year, refdate)
if fout:
return False, (f"{naam}: {fout}" if len(self.campussen) > 1 else fout)
for st in lijst:
sleutel = str(st.get('pPersoon') or st.get('internNummer') or st.get('id', '')).strip()
if sleutel and sleutel not in gezien:
gezien.add(sleutel)
students_list.append(st)
except Exception as e:
return False, f"Fout opgetreden tijdens de synchronisatie: {str(e)}"
if not students_list:
# Nooit iedereen als vertrokken markeren omdat Informat (tijdelijk) een lege lijst gaf
return False, "Informat gaf geen enkele actieve leerling terug; er is niets aangepast."
# Smartschool (gebruikersnaam en Smartschool-nummer). Lukt dat niet, dan blijven die velden zoals ze waren.
ss_leerlingen, ss_fout = self.smartschool_koppeling()
try:
print(f"[DEBUG] Aantal actieve leerlingen gevonden in Informat: {len(students_list)}")
zorg_vertrokken_kolom()
db_rows = execute_query("SELECT Internnummer FROM Leerlingen")
bestaande_internnummers = {str(row['Internnummer']).strip() for row in db_rows} if db_rows else set()
toegevoegd_teller = bijgewerkt_teller = 0
schooljaar_id = Config.SCHOOLYEAR[:4]
actief = set()
for student in students_list:
internnummer = str(student.get('pPersoon') or student.get('internNummer') or student.get('id', '')).strip()
voornaam = (student.get('voornaam' if 'voornaam' in student else 'firstName') or '').strip()
naam = (student.get('naam' if 'naam' in student else 'lastName') or '').strip()
if not internnummer or internnummer == "None":
continue
actief.add(internnummer)
ss_info = ss_leerlingen.get(internnummer)
standaard_gebruikersnaam = f"{voornaam}.{naam}".replace(" ", "").lower()
if internnummer in bestaande_internnummers:
if ss_info:
execute_query("""
UPDATE Leerlingen SET Gebruikersnaam = %s, Voornaam = %s, Naam = %s, SS_nummer = %s,
Schooljaar_ID = %s, Vertrokken_op = NULL
WHERE Internnummer = %s""",
(ss_info.get("username") or standaard_gebruikersnaam, voornaam, naam,
ss_info.get("ss_nummer") or "Niet ingesteld", schooljaar_id, internnummer), commit=True)
else:
# Niet (of niet bereikbaar) in Smartschool: gebruikersnaam en Smartschool-nummer niet overschrijven
execute_query("""
UPDATE Leerlingen SET Voornaam = %s, Naam = %s, Schooljaar_ID = %s, Vertrokken_op = NULL
WHERE Internnummer = %s""", (voornaam, naam, schooljaar_id, internnummer), commit=True)
bijgewerkt_teller += 1
else:
ss_info = ss_info or {}
execute_query("""
INSERT INTO Leerlingen (Internnummer, Gebruikersnaam, Voornaam, Naam, SS_nummer, Schooljaar_ID)
VALUES (%s, %s, %s, %s, %s, %s)""",
(internnummer, ss_info.get("username") or standaard_gebruikersnaam, voornaam, naam,
ss_info.get("ss_nummer") or "Niet ingesteld", schooljaar_id), commit=True)
bestaande_internnummers.add(internnummer)
toegevoegd_teller += 1
# Leerlingen die niet meer actief zijn in Informat: als vertrokken markeren (niet verwijderen)
nog_actief = execute_query("SELECT Internnummer FROM Leerlingen WHERE Vertrokken_op IS NULL") or []
weg = [r['Internnummer'] for r in nog_actief if str(r['Internnummer']).strip() not in actief]
for k in range(0, len(weg), 500):
deel = weg[k:k + 500]
execute_query(f"UPDATE Leerlingen SET Vertrokken_op = CURDATE() WHERE Internnummer IN "
f"({', '.join(['%s'] * len(deel))})", tuple(deel), commit=True)
melding = f"{toegevoegd_teller} nieuw, {bijgewerkt_teller} bijgewerkt"
if weg:
melding += f", {len(weg)} als vertrokken gemarkeerd"
return True, {'melding': melding + '.', 'smartschool_fout': ss_fout}
except Exception as e:
return False, f"Fout opgetreden tijdens de synchronisatie: {str(e)}"