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