From 65fadf7f5fa8fc05ef569c42f1d5bf152459917e Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Sun, 23 Aug 2026 11:28:53 +0200 Subject: [PATCH] impolementation of standardised time zone --- Web/app.py | 530 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 380 insertions(+), 150 deletions(-) diff --git a/Web/app.py b/Web/app.py index a2911eb..d476543 100755 --- a/Web/app.py +++ b/Web/app.py @@ -95,6 +95,7 @@ import Web.modules.database.settings as cfg from Web.modules.database.settings import MongoClient from tenant import get_tenant_context, get_tenant_db, get_tenant_trial_status, purge_expired_trial_tenants from pymongo.errors import DuplicateKeyError +from zoneinfo import ZoneInfo app = Flask(__name__, static_folder='static') # Correctly set static folder @@ -199,7 +200,7 @@ def rollover_student_card_classes(dry_run=False, *, max_class=None, graduate_lab client = MongoClient(MONGODB_HOST, MONGODB_PORT) db = client[MONGODB_DB] col = db['student_cards'] - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) cursor = list(col.find({}, {'Klasse': 1})) for doc in cursor: @@ -269,7 +270,7 @@ def api_library_return_by_code(): return jsonify({'ok': False, 'message': 'Kein Bibliotheksmedium für diesen Code gefunden.'}), 404 item_id = str(item_doc['_id']) - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) # If item already available -> nothing to return if item_doc.get('Verfuegbar', True): @@ -1025,7 +1026,7 @@ def _prepare_invoice_pdf_payload(invoice_data, borrow_doc=None, item_doc=None): item_id = str(item_doc.get('_id')) return { - 'invoice_number': invoice_data.get('invoice_number') or _build_invoice_number(borrow_doc.get('_id', ''), datetime.datetime.now()), + 'invoice_number': invoice_data.get('invoice_number') or _build_invoice_number(borrow_doc.get('_id', ''), datetime.datetime.now(ZoneInfo("Europe/Berlin"))), 'created_at': created_at_raw, 'created_at_display': created_at_display, 'borrower': invoice_data.get('borrower') or borrow_doc.get('User', '-'), @@ -1048,7 +1049,7 @@ def _create_notification(db, *, audience, notif_type, title, message, target_use if existing: return False - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) payload = { 'Audience': audience, 'Type': notif_type, @@ -1278,7 +1279,7 @@ def _build_reminder_message(item_name, start_dt=None, end_dt=None): def create_return_reminders(): """Create one-time reminders for day-1 and planned-end events.""" - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) one_day_ago = now - datetime.timedelta(days=1) client = None @@ -1456,7 +1457,7 @@ def update_appointment_statuses(): - Geplante Termine, die aktiviert werden sollten - Aktive Termine, die beendet werden sollten """ - current_time = datetime.datetime.now(datetime.timezone.utc) + current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin")) try: # Hole alle Termine mit Status 'planned' oder 'active' @@ -1589,7 +1590,105 @@ def update_appointment_statuses(): f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}" ) elif it.is_library_item(appointment.get('Item')): - create_return_reminders() + current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin")) + + try: + client = MongoClient(MONGODB_HOST, MONGODB_PORT) + db = client[MONGODB_DB] + ausleihungen = db['ausleihungen'] + users_col = db['users'] + items_col = db['items'] + + # Finde alle aktiven Ausleihungen, die in der Vergangenheit fällig waren + overdue_appointments = list(ausleihungen.find({ + 'Status': 'active', + 'DueDate': {'$lt': current_time} + })) + + for appt in overdue_appointments: + days_overdue = (current_time - appt.get('DueDate', current_time)).days + mahnstufe = appt.get('Mahnstufe', 0) + target_user = str(appt.get('User', '')).strip() + item_id = appt.get('Item') + + # 1. Objektdetails holen (für den E-Mail-Text) + item_name = "Unbekannter Artikel" + if item_id: + try: + item_doc = items_col.find_one({'_id': ObjectId(item_id)}) + if item_doc: + item_name = item_doc.get('Name', str(item_id)) + except Exception: + pass + + # 2. Nutzerdetails holen (für die E-Mail-Adresse) + user_doc = users_col.find_one({'username': target_user}) # Feldnamen ggf. anpassen (z.B. '_id') + if not user_doc: + app.logger.warning(f"Nutzer '{target_user}' für Mahnung nicht gefunden.") + continue + + user_email = user_doc.get('email') + + # 3. Eskalationsstufen prüfen + if days_overdue >= 14 and days_overdue < 28 and mahnstufe == 0: + # STUFE 1: Erste Mahnung + ausleihungen.update_one( + {'_id': appt['_id']}, + {'$set': {'Mahnstufe': 1, 'LastUpdated': current_time}} + ) + + _create_notification( + db, audience='user', notif_type='warning', + title='Erinnerung: Ausleihe überfällig', + message=f'Deine Ausleihe für "{item_name}" ist seit {days_overdue} Tagen überfällig.', + target_user=target_user, severity='warning' + ) + + if user_email: + subject = "1. Mahnung: Rückgabe überfällig" + note = (f"Hallo {target_user},

" + f"bitte beachte, dass die Ausleihe für den Artikel {item_name} " + f"seit {days_overdue} Tagen überfällig ist. Bitte bringe den Artikel zeitnah zurück.") + send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung") + app.logger.info(f"1. Mahnung an {user_email} gesendet.") + + elif days_overdue >= 28 and mahnstufe == 1: + # STUFE 2: Letzte Mahnung, Sperrung und Report + ausleihungen.update_one( + {'_id': appt['_id']}, + {'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}} + ) + + # Nutzerkonto für weitere Ausleihen sperren + users_col.update_one( + {'_id': user_doc['_id']}, + {'$set': { + 'is_blocked': True, + 'block_reason': f'System-Sperre: Ausleihe von "{item_name}" {days_overdue} Tage überfällig.' + }} + ) + + _create_notification( + db, audience='user', notif_type='error', + title='Konto gesperrt - 2. Mahnung', + message=f'Aufgrund der starken Überfälligkeit von "{item_name}" wurde dein Konto vorübergehend gesperrt.', + target_user=target_user, severity='critical' + ) + + if user_email: + subject = "WICHTIG: Kontosperrung & 2. Mahnung" + note = (f"Hallo {target_user},

" + f"deine Ausleihe für den Artikel {item_name} ist nun seit {days_overdue} Tagen überfällig. " + f"Dies ist ein automatischer Bericht über deinen Status: Dein Konto wurde soeben für neue Reservierungen gesperrt.

" + f"Die Sperre wird erst aufgehoben, sobald der Artikel im System als zurückgegeben gemeldet wurde.") + send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung") + app.logger.warning( + f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.") + + client.close() + + except Exception as e: + app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}") client.close() @@ -1621,7 +1720,7 @@ def _initialize_scheduler(): # 1. Generate a unique ID for this specific worker process _scheduler_worker_id = str(uuid.uuid4()) - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) # 2. Ensure the lock document exists (initialize if missing) try: @@ -1665,7 +1764,7 @@ def _initialize_scheduler(): hb_db = hb_client[MONGODB_DB] hb_db['system_locks'].update_one( {'_id': 'scheduler_lock', 'worker_id': _scheduler_worker_id}, - {'$set': {'locked_at': datetime.datetime.now(datetime.timezone.utc)}} + {'$set': {'locked_at': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}} ) hb_client.close() except Exception as e: @@ -1714,110 +1813,6 @@ def _shutdown_scheduler(): atexit.register(_shutdown_scheduler) -def create_return_reminders(): - """ - Prüft aktive Ausleihungen auf Überschreitung der Rückgabefrist, - erhöht Mahnstufen, versendet E-Mails/Benachrichtigungen und sperrt Nutzer. - """ - current_time = datetime.datetime.now(datetime.timezone.utc) - - try: - client = MongoClient(MONGODB_HOST, MONGODB_PORT) - db = client[MONGODB_DB] - ausleihungen = db['ausleihungen'] - users_col = db['users'] - items_col = db['items'] - - # Finde alle aktiven Ausleihungen, die in der Vergangenheit fällig waren - overdue_appointments = list(ausleihungen.find({ - 'Status': 'active', - 'DueDate': {'$lt': current_time} - })) - - for appt in overdue_appointments: - days_overdue = (current_time - appt.get('DueDate', current_time)).days - mahnstufe = appt.get('Mahnstufe', 0) - target_user = str(appt.get('User', '')).strip() - item_id = appt.get('Item') - - # 1. Objektdetails holen (für den E-Mail-Text) - item_name = "Unbekannter Artikel" - if item_id: - try: - item_doc = items_col.find_one({'_id': ObjectId(item_id)}) - if item_doc: - item_name = item_doc.get('Name', str(item_id)) - except Exception: - pass - - # 2. Nutzerdetails holen (für die E-Mail-Adresse) - user_doc = users_col.find_one({'username': target_user}) # Feldnamen ggf. anpassen (z.B. '_id') - if not user_doc: - app.logger.warning(f"Nutzer '{target_user}' für Mahnung nicht gefunden.") - continue - - user_email = user_doc.get('email') - - # 3. Eskalationsstufen prüfen - if days_overdue >= 14 and days_overdue < 28 and mahnstufe == 0: - # STUFE 1: Erste Mahnung - ausleihungen.update_one( - {'_id': appt['_id']}, - {'$set': {'Mahnstufe': 1, 'LastUpdated': current_time}} - ) - - _create_notification( - db, audience='user', notif_type='warning', - title='Erinnerung: Ausleihe überfällig', - message=f'Deine Ausleihe für "{item_name}" ist seit {days_overdue} Tagen überfällig.', - target_user=target_user, severity='warning' - ) - - if user_email: - subject = "1. Mahnung: Rückgabe überfällig" - note = (f"Hallo {target_user},

" - f"bitte beachte, dass die Ausleihe für den Artikel {item_name} " - f"seit {days_overdue} Tagen überfällig ist. Bitte bringe den Artikel zeitnah zurück.") - send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung") - app.logger.info(f"1. Mahnung an {user_email} gesendet.") - - elif days_overdue >= 28 and mahnstufe == 1: - # STUFE 2: Letzte Mahnung, Sperrung und Report - ausleihungen.update_one( - {'_id': appt['_id']}, - {'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}} - ) - - # Nutzerkonto für weitere Ausleihen sperren - users_col.update_one( - {'_id': user_doc['_id']}, - {'$set': { - 'is_blocked': True, - 'block_reason': f'System-Sperre: Ausleihe von "{item_name}" {days_overdue} Tage überfällig.' - }} - ) - - _create_notification( - db, audience='user', notif_type='error', - title='Konto gesperrt - 2. Mahnung', - message=f'Aufgrund der starken Überfälligkeit von "{item_name}" wurde dein Konto vorübergehend gesperrt.', - target_user=target_user, severity='critical' - ) - - if user_email: - subject = "WICHTIG: Kontosperrung & 2. Mahnung" - note = (f"Hallo {target_user},

" - f"deine Ausleihe für den Artikel {item_name} ist nun seit {days_overdue} Tagen überfällig. " - f"Dies ist ein automatischer Bericht über deinen Status: Dein Konto wurde soeben für neue Reservierungen gesperrt.

" - f"Die Sperre wird erst aufgehoben, sobald der Artikel im System als zurückgegeben gemeldet wurde.") - send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung") - app.logger.warning(f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.") - - client.close() - - except Exception as e: - app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}") - """-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """ def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB): @@ -2501,7 +2496,7 @@ def _upload_student_cards_excel(): student_cards_col.insert_one({ 'AusweisId': row['ausweis_id'], 'StandardAusleihdauer': int(row['default_borrow_days']), - 'Erstellt': datetime.datetime.now(), + 'Erstellt': datetime.datetime.now(ZoneInfo("Europe/Berlin")), **encrypted_payload, }) created_total += 1 @@ -3635,10 +3630,246 @@ def library_loans_admin(): client.close() +def generate_test_ausleihen(): + """ + Generiert Testdaten für die Überprüfung der Mahnungs- und Sperrlogik. + Erstellt Dummy-Nutzer, Dummy-Items und Ausleihungen mit manipulierten Daten (DueDate). + """ + current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin")) + + try: + client = MongoClient(MONGODB_HOST, MONGODB_PORT) + db = client[MONGODB_DB] + ausleihungen = db['ausleihungen'] + users_col = db['users'] + items_col = db['items'] + + print("Generiere Testdaten...") + + # 1. Test-Nutzer erstellen oder aktualisieren (upsert) + test_users = [ + {"username": "test_user_stufe1", "email": "stufe1@test.local", "is_blocked": False}, + {"username": "test_user_stufe2", "email": "stufe2@test.local", "is_blocked": False}, + {"username": "test_user_frisch", "email": "frisch@test.local", "is_blocked": False} + ] + + for u in test_users: + users_col.update_one( + {'username': u['username']}, + {'$set': u}, + upsert=True + ) + + # 2. Test-Items (Gegenstände) erstellen + item1_res = items_col.insert_one({"Name": "Biologie Buch (Test Stufe 1)"}) + item2_res = items_col.insert_one({"Name": "iPad Pro (Test Stufe 2)"}) + item3_res = items_col.insert_one({"Name": "Taschenrechner (Test Frisch)"}) + + # 3. Test-Ausleihungen generieren + # Fall 1: 20 Tage drüber, Mahnstufe 0 -> Sollte Stufe 1 triggern + due_date_stufe1 = current_time - datetime.timedelta(days=20) + + # Fall 2: 30 Tage drüber, Mahnstufe 1 -> Sollte Stufe 2 triggern & Nutzer sperren + due_date_stufe2 = current_time - datetime.timedelta(days=30) + + # Fall 3: 5 Tage drüber, Mahnstufe 0 -> Sollte NICHTS tun (da < 14 Tage) + due_date_frisch = current_time - datetime.timedelta(days=5) + + test_ausleihungen = [ + { + "User": encrypt_text("test_user_stufe1"), + "Item": str(item1_res.inserted_id), + "Status": "active", + "DueDate": due_date_stufe1, + "Mahnstufe": 0, + "LastUpdated": current_time + }, + { + "User": "test_user_stufe2", + "Item": str(item2_res.inserted_id), + "Status": "active", + "DueDate": due_date_stufe2, + "Mahnstufe": 1, # Hat bereits Stufe 1 + "LastUpdated": due_date_stufe2 # Wurde in der Vergangenheit gemahnt + }, + { + "User": "test_user_frisch", + "Item": str(item3_res.inserted_id), + "Status": "active", + "DueDate": due_date_frisch, + "Mahnstufe": 0, + "LastUpdated": current_time + } + ] + + # In die Datenbank einfügen + ausleihungen.insert_many(test_ausleihungen) + + print("Erfolgreich! Folgende Testdaten wurden in die DB geschrieben:") + print( + f"- Fall 1 (Mahnstufe 1 Trigger): Nutzer 'test_user_stufe1', fällig war am {due_date_stufe1.strftime('%Y-%m-%d')}") + print( + f"- Fall 2 (Mahnstufe 2 & Sperre Trigger): Nutzer 'test_user_stufe2', fällig war am {due_date_stufe2.strftime('%Y-%m-%d')}") + print( + f"- Fall 3 (Ignorieren - zu früh): Nutzer 'test_user_frisch', fällig war am {due_date_frisch.strftime('%Y-%m-%d')}") + + except Exception as e: + print(f"Fehler beim Erstellen der Testdaten: {e}") + finally: + client.close() + @app.route('/test_mahnungen') def test_mahnungen(): # Testdaten (Mock-Daten), um alle if/else Bedingungen im HTML zu testen - test_overdue_list = [ + generate_test_ausleihen() + create_return_reminders() + """Admin overview for overdue library items (Mahnungen).""" + if 'username' not in session: + flash( + 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', + 'error') + return redirect(url_for('login')) + + current_permissions = us.get_effective_permissions(session['username']) + + # Hier nutzen wir beispielhaft die library_loans_admin Berechtigung + if not current_permissions['pages'].get('library_loans_admin', False): + flash( + 'Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', + 'error') + return redirect(url_for('library_view')) + + if not cfg.MODULES.is_enabled('library'): + flash('Bibliotheks-Modul ist deaktiviert.', 'error') + return redirect(url_for('home_admin')) + + def fmt_dt(dt): + try: + return dt.strftime('%d.%m.%Y') if dt else 'Unbekannt' + except Exception: + return str(dt) if dt else 'Unbekannt' + + def safe_decrypt(val): + if not val or not isinstance(val, str): + return val or '' + try: + return decrypt_text(val) + except Exception: + return val + + current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin")) + client = None + + try: + client = MongoClient(MONGODB_HOST, MONGODB_PORT) + db = client[MONGODB_DB] + ausleihungen_col = db['ausleihungen'] + items_col = db['items'] + users_col = db['users'] + student_cards_col = db['student_cards'] + + # Überfällige Ausleihen abrufen (Status 'active' und DueDate in der Vergangenheit) + overdue_records = list(ausleihungen_col.find({ + 'Status': 'active', + 'DueDate': {'$lt': current_time} + }).sort('DueDate', 1)) + + overdue_list = [] + + if overdue_records: + # Bulk-Lookups zur Vermeidung von N+1 Queries + item_ids = [] + for r in overdue_records: + i_id = r.get('Item') + if i_id: + try: + item_ids.append(ObjectId(str(i_id))) + except Exception: + item_ids.append(str(i_id)) + + items_cursor = items_col.find({ + '_id': {'$in': item_ids} + }, {'Name': 1, 'Code_4': 1}) + item_map = {str(item['_id']): item for item in items_cursor} + + raw_users = [r.get('User') for r in overdue_records if r.get('User')] + users_cursor = users_col.find({'username': {'$in': raw_users}}, {'username': 1, 'is_blocked': 1}) + user_block_map = {u.get('username'): u.get('is_blocked', False) for u in users_cursor} + + class_map = {} + all_cards = list(student_cards_col.find({}, {'SchülerName': 1, 'Klasse': 1, 'AusweisId': 1})) + for card in all_cards: + raw_cls = card.get('Klasse') + dec_cls = safe_decrypt(raw_cls) + if not dec_cls: + continue + + raw_name = card.get('SchülerName') + dec_name = safe_decrypt(raw_name) + ausweis_id = card.get('AusweisId') + + if dec_name: + class_map[dec_name.strip().lower()] = dec_cls + if raw_name: + class_map[raw_name] = dec_cls + if ausweis_id: + class_map[str(ausweis_id).strip().lower()] = dec_cls + + # Datenaufbereitung für das Template + for record in overdue_records: + item_id = str(record.get('Item') or '') + item_doc = item_map.get(item_id, {}) + item_name = item_doc.get('Name', item_id) + item_code = item_doc.get('Code_4', '') + + if item_code: + item_name = f"{item_name} ({item_code})" + + raw_user = record.get('User', '') + decrypted_user = safe_decrypt(raw_user) + display_user = decrypted_user if decrypted_user else (raw_user or 'Unbekannt') + + user_class = ( + safe_decrypt(record.get('Klasse')) + or safe_decrypt(record.get('Class')) + or safe_decrypt(record.get('school_class')) + or class_map.get(raw_user, '') + or class_map.get(decrypted_user, '') + or class_map.get(display_user.strip().lower(), '') + or '—' + ) + + due_date_obj = record.get('DueDate') + days_overdue = (current_time - due_date_obj).days if due_date_obj else 0 + is_blocked = user_block_map.get(raw_user, False) + + overdue_list.append({ + 'id': str(record.get('_id')), + 'item_name': item_name, + 'user': display_user, + 'klasse': user_class, + 'due_date': fmt_dt(due_date_obj), + 'days_overdue': days_overdue, + 'mahnstufe': record.get('Mahnstufe', 0), + 'is_blocked': is_blocked + }) + + return render_template( + 'mahnungen_admin.html', + overdue_list=overdue_list, + library_module_enabled=cfg.MODULES.is_enabled('library'), + student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'), + ) + + except Exception as e: + app.logger.error(f"Error loading mahnungen admin view: {e}") + flash('Fehler beim Laden der Mahnungsverwaltung.', 'error') + return redirect(url_for('home_admin')) + finally: + if client: + client.close() + + """ test_overdue_list = [ { "user": "Max Mustermann", "klasse": "10A", @@ -3669,7 +3900,7 @@ def test_mahnungen(): ] # Render das Template und übergebe die Testdaten - return render_template('mahnungen_admin.html', overdue_list=test_overdue_list, APP_VERSION="1.0.0") + return render_template('mahnungen_admin.html', overdue_list=test_overdue_list, APP_VERSION="1.0.0")""" @app.route('/mahnungen_admin') @@ -3708,7 +3939,7 @@ def mahnungen_admin(): except Exception: return val - current_time = datetime.datetime.now(datetime.timezone.utc) + current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin")) client = None try: @@ -4119,7 +4350,7 @@ def api_library_scan_action(): item_id = str(item_doc['_id']) borrower_name = card_doc.get('SchülerName') or f"Ausweis {student_card_id}" - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) if item_doc.get('Verfuegbar', True): borrow_duration_days = None @@ -4142,9 +4373,8 @@ def api_library_scan_action(): card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_BORROW_DAYS)) - end_date = now + datetime.timedelta(days=borrow_duration_days) if borrow_duration_days else None it.update_item_status(item_id, False, borrower_name) - au.add_ausleihung(item_id, borrower_name, now, end_date=end_date) + au.add_ausleihung(item_id, borrower_name, now) _append_audit_event_standalone( event_type='ausleihung_borrowed', @@ -4406,7 +4636,7 @@ def api_library_item_update(item_id): 'Autor': author, 'Code_4': code_4, 'ItemType': media_type, - 'LastUpdated': datetime.datetime.now() + 'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin")) } if normalized_isbn: @@ -4600,7 +4830,7 @@ def student_cards_admin(): {'$set': { 'AusweisId': ausweis_id, 'StandardAusleihdauer': int(default_borrow_days), - 'Aktualisiert': datetime.datetime.now(), + 'Aktualisiert': datetime.datetime.now(ZoneInfo("Europe/Berlin")), **encrypted_payload }} ) @@ -4639,7 +4869,7 @@ def student_cards_admin(): student_cards.insert_one({ 'AusweisId': ausweis_id, 'StandardAusleihdauer': int(default_borrow_days), - 'Erstellt': datetime.datetime.now(), + 'Erstellt': datetime.datetime.now(ZoneInfo("Europe/Berlin")), **encrypted_payload, }) flash('Neuer Ausweis wurde hinzugefügt.', 'success') @@ -4696,7 +4926,7 @@ def student_cards_print(): return render_template( 'student_cards_print.html', student_cards=all_cards, - current_datetime=datetime.datetime.now() + current_datetime=datetime.datetime.now(ZoneInfo("Europe/Berlin")) ) @@ -4730,7 +4960,7 @@ def student_card_barcode_print(): return render_template( 'student_card_barcode_print.html', student_cards=all_cards, - current_datetime=datetime.datetime.now(), + current_datetime=datetime.datetime.now(ZoneInfo("Europe/Berlin")), download_link=url_for('student_card_barcode_download') ) @@ -4926,7 +5156,7 @@ def student_card_barcode_download(): pdf_buffer, mimetype='application/pdf', as_attachment=True, - download_name=f'schuelerausweise_all_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.pdf' + download_name=f'schuelerausweise_all_{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}.pdf' ) except Exception as e: app.logger.error(f"Error occurred while generating PDF for card {card['AusweisId']}: {e}") @@ -5112,7 +5342,7 @@ def student_card_class_barcode_download(): c.save() pdf_buffer.seek(0) - filename = f'ausweise_klasse_{class_name.replace(" ", "_")}_{datetime.datetime.now().strftime("%Y%m%d")}.pdf' + filename = f'ausweise_klasse_{class_name.replace(" ", "_")}_{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d")}.pdf' return send_file( pdf_buffer, @@ -6395,7 +6625,7 @@ def upload_item(): def _soft_delete_item_groups(db, root_item_ids, username): """Soft-delete one or more item groups and their borrow records.""" - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) unique_group_item_ids = [] seen_ids = set() @@ -6916,7 +7146,7 @@ def update_group(): # 1. Shared Fields (Group Logic) # These apply to every item in the group - shared_update = {'LastUpdated': datetime.datetime.now()} + shared_update = {'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))} for source_key, target_key in ( ('name', 'Name'), ('ort', 'Ort'), @@ -6991,7 +7221,7 @@ def report_damage(id): if not item_doc: return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404 - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) damage_entry = { 'description': description, 'reported_by': session['username'], @@ -7095,7 +7325,7 @@ def mark_damage_repaired(id): return jsonify({'success': False, 'message': 'Keine offenen Schäden vorhanden.'}), 400 active_borrow = ausleihungen_col.find_one({'Item': str(id), 'Status': 'active'}, {'_id': 1}) - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) repair_entry = { 'repaired_by': session['username'], 'repaired_at': now, @@ -7272,7 +7502,7 @@ def ausleihen(id): card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_BORROW_DAYS)) - start_date = datetime.datetime.now() + start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")) end_date = None if borrow_duration_days: end_date = start_date + datetime.timedelta(days=borrow_duration_days) @@ -7361,7 +7591,7 @@ def ausleihen(id): # Before borrowing, block if there's a conflicting planned booking try: - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) # Fetch planned bookings for this item from DB planned = au.get_planned_ausleihungen() # Count relevant upcoming planned bookings for today or ongoing @@ -7422,12 +7652,12 @@ def ausleihen(id): return redirect(url_for(redirect_target)) # If we reach here, we can borrow the requested number of exemplars - current_date = datetime.datetime.now().strftime('%d.%m.%Y %H:%M') + current_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%d.%m.%Y %H:%M') # If the item doesn't use exemplars (single item) if total_exemplare <= 1: it.update_item_status(id, False, effective_borrower) - start_date = datetime.datetime.now() + start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")) au.add_ausleihung(id, effective_borrower, start_date, end_date=end_date) _append_audit_event_standalone( event_type='ausleihung_returned', @@ -7470,7 +7700,7 @@ def ausleihen(id): it.update_item_status(id, False, username) # Create ausleihung records for each borrowed exemplar - start_date = datetime.datetime.now() + start_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")) for exemplar in new_borrowed_exemplars: exemplar_id = f"{id}_{exemplar['number']}" au.add_ausleihung(exemplar_id, effective_borrower, start_date, end_date=end_date, exemplar_data={ @@ -7532,7 +7762,7 @@ def zurueckgeben(id): 'Status': 'active' }) - end_date = datetime.datetime.now() + end_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")) original_user = item.get('User', username) updated_count = 0 @@ -7544,7 +7774,7 @@ def zurueckgeben(id): {'$set': { 'Status': 'completed', 'End': end_date, - 'LastUpdated': datetime.datetime.now() + 'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin")) }} ) @@ -7771,7 +8001,7 @@ def check_availability(): # Also include current availability if checking today and item is borrowed now item_doc = items_col.find_one({'_id': ObjectId(item_id)}) if item_doc and not item_doc.get('Verfuegbar', True): - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) if req_start.date() == now.date(): conflicts.append({'status': 'active', 'user': item_doc.get('User'), 'start': None, 'end': None, 'period': None, 'id': None}) @@ -8464,7 +8694,7 @@ def delete_user(): items_col = db['items'] users_col = db['users'] # Direkter Zugriff auf die User-Collection - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) # 1. Aktive Ausleihen abschließen ausleihungen.update_many( @@ -8906,7 +9136,7 @@ def admin_reset_borrowing(borrow_id): item_id = rec.get('Item') user = rec.get('User') - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) if status == 'active': ausleihungen.update_one({'_id': rec['_id']}, {'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}}) # Free the item @@ -9004,7 +9234,7 @@ def admin_create_invoice(borrow_id): mark_destroyed = request.form.get('mark_destroyed') == 'on' close_borrowing = request.form.get('close_borrowing') == 'on' - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) existing_invoice = borrow_doc.get('InvoiceData') or {} if existing_invoice.get('invoice_number'): @@ -9173,7 +9403,7 @@ def admin_mark_invoice_paid(borrow_id): flash('Rechnung ist bereits als bezahlt markiert.', 'info') return redirect(url_for('admin_borrowings')) - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) result = ausleihungen.update_one( {'_id': borrow_doc['_id']}, { @@ -9252,7 +9482,7 @@ def admin_pay_invoice_extended(borrow_id): flash('Für diese Ausleihung existiert keine Rechnung.', 'warning') return redirect(request.referrer or url_for('library_loans_admin')) - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) update_fields = {'LastUpdated': now} if invoice_data.get('paid') is not True: @@ -9322,7 +9552,7 @@ def resolve_repaired_item(item_id): flash('Element nicht gefunden.', 'error') return redirect(request.referrer or url_for('library_loans_admin')) - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) open_reports = item_doc.get('DamageReports', []) or [] item_update = { @@ -9459,7 +9689,7 @@ def admin_add_invoice_correction(borrow_id): flash('Ungültiger Korrekturbetrag.', 'error') return redirect(url_for('admin_borrowings')) - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) correction_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}" correction_entry = { @@ -9549,7 +9779,7 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad return False, "Item nicht in der Datenbank gefunden." series_group_id = item.get('SeriesGroupId') - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) legacy_damage_unset = { 'has_damage': "", @@ -10060,7 +10290,7 @@ def admin_anonymize_names(): result = student_cards_col.update_one( {'_id': card_doc['_id']}, - {'$set': {'Aktualisiert': datetime.datetime.now(), **encrypted_payload}} + {'$set': {'Aktualisiert': datetime.datetime.now(ZoneInfo("Europe/Berlin")), **encrypted_payload}} ) if result.modified_count > 0: cards_updated += 1 @@ -11174,7 +11404,7 @@ def mark_notification_read(notification_id): {'_id': ObjectId(notification_id)}, { '$addToSet': {'ReadBy': username}, - '$set': {'UpdatedAt': datetime.datetime.now()} + '$set': {'UpdatedAt': datetime.datetime.now(ZoneInfo("Europe/Berlin"))} } ) if result.modified_count > 0: @@ -11814,7 +12044,7 @@ def schedule_appointment(): return jsonify({'success': False, 'message': f'Fehler beim Prüfen der Verfügbarkeit'}), 500 # Check if the appointment should already be active - now = datetime.datetime.now() + now = datetime.datetime.now(ZoneInfo("Europe/Berlin")) initial_status = 'active' if start_datetime <= now else 'planned' # Create the appointment