impolementation of standardised time zone
Release Inventarsystem / release-docker (push) Successful in 2m17s
Release Inventarsystem / release-docker (push) Successful in 2m17s
This commit is contained in:
+405
-175
@@ -95,6 +95,7 @@ import Web.modules.database.settings as cfg
|
|||||||
from Web.modules.database.settings import MongoClient
|
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 tenant import get_tenant_context, get_tenant_db, get_tenant_trial_status, purge_expired_trial_tenants
|
||||||
from pymongo.errors import DuplicateKeyError
|
from pymongo.errors import DuplicateKeyError
|
||||||
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__, static_folder='static') # Correctly set static folder
|
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)
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
db = client[MONGODB_DB]
|
db = client[MONGODB_DB]
|
||||||
col = db['student_cards']
|
col = db['student_cards']
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
|
|
||||||
cursor = list(col.find({}, {'Klasse': 1}))
|
cursor = list(col.find({}, {'Klasse': 1}))
|
||||||
for doc in cursor:
|
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
|
return jsonify({'ok': False, 'message': 'Kein Bibliotheksmedium für diesen Code gefunden.'}), 404
|
||||||
|
|
||||||
item_id = str(item_doc['_id'])
|
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 already available -> nothing to return
|
||||||
if item_doc.get('Verfuegbar', True):
|
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'))
|
item_id = str(item_doc.get('_id'))
|
||||||
|
|
||||||
return {
|
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': created_at_raw,
|
||||||
'created_at_display': created_at_display,
|
'created_at_display': created_at_display,
|
||||||
'borrower': invoice_data.get('borrower') or borrow_doc.get('User', '-'),
|
'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:
|
if existing:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
payload = {
|
payload = {
|
||||||
'Audience': audience,
|
'Audience': audience,
|
||||||
'Type': notif_type,
|
'Type': notif_type,
|
||||||
@@ -1278,7 +1279,7 @@ def _build_reminder_message(item_name, start_dt=None, end_dt=None):
|
|||||||
|
|
||||||
def create_return_reminders():
|
def create_return_reminders():
|
||||||
"""Create one-time reminders for day-1 and planned-end events."""
|
"""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)
|
one_day_ago = now - datetime.timedelta(days=1)
|
||||||
|
|
||||||
client = None
|
client = None
|
||||||
@@ -1456,7 +1457,7 @@ def update_appointment_statuses():
|
|||||||
- Geplante Termine, die aktiviert werden sollten
|
- Geplante Termine, die aktiviert werden sollten
|
||||||
- Aktive Termine, die beendet 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:
|
try:
|
||||||
# Hole alle Termine mit Status 'planned' oder 'active'
|
# Hole alle Termine mit Status 'planned' oder 'active'
|
||||||
@@ -1589,137 +1590,7 @@ def update_appointment_statuses():
|
|||||||
f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}"
|
f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}"
|
||||||
)
|
)
|
||||||
elif it.is_library_item(appointment.get('Item')):
|
elif it.is_library_item(appointment.get('Item')):
|
||||||
create_return_reminders()
|
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
if updated_count > 0:
|
|
||||||
app.logger.warning(
|
|
||||||
f"Appointment status update finished: {updated_count} changed ({activated_count} active, {completed_count} completed)"
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"Automatic appointment status update failed: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# Initialize scheduler instances
|
|
||||||
scheduler = BackgroundScheduler()
|
|
||||||
_scheduler_initialized = False
|
|
||||||
_scheduler_worker_id = None # Tracks the unique ID of the worker holding the lock
|
|
||||||
|
|
||||||
|
|
||||||
def _initialize_scheduler():
|
|
||||||
"""Initialize the background scheduler safely using an atomic MongoDB lock."""
|
|
||||||
global _scheduler_initialized, _scheduler_worker_id
|
|
||||||
if _scheduler_initialized or not getattr(cfg, 'SCHEDULER_ENABLED', False):
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
|
||||||
db = client[MONGODB_DB]
|
|
||||||
locks_col = db['system_locks']
|
|
||||||
|
|
||||||
# 1. Generate a unique ID for this specific worker process
|
|
||||||
_scheduler_worker_id = str(uuid.uuid4())
|
|
||||||
now = datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
|
|
||||||
# 2. Ensure the lock document exists (initialize if missing)
|
|
||||||
try:
|
|
||||||
locks_col.insert_one({
|
|
||||||
'_id': 'scheduler_lock',
|
|
||||||
'locked_at': datetime.datetime.min.replace(tzinfo=datetime.timezone.utc),
|
|
||||||
'worker_id': None
|
|
||||||
})
|
|
||||||
except DuplicateKeyError:
|
|
||||||
pass # Document already exists, which is expected
|
|
||||||
|
|
||||||
# 3. Try to acquire the lock atomically
|
|
||||||
# We only acquire if the current lock is older than 5 minutes (stale/crashed worker)
|
|
||||||
# or if it was explicitly released (1970 init date)
|
|
||||||
lock_timeout = now - datetime.timedelta(minutes=5)
|
|
||||||
|
|
||||||
acquired = locks_col.find_one_and_update(
|
|
||||||
{
|
|
||||||
'_id': 'scheduler_lock',
|
|
||||||
'locked_at': {'$lt': lock_timeout}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'$set': {
|
|
||||||
'locked_at': now,
|
|
||||||
'worker_id': _scheduler_worker_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if acquired is not None:
|
|
||||||
# 4. Lock acquired successfully - this is the master worker
|
|
||||||
scheduler.add_job(func=create_daily_backup, trigger="interval", hours=cfg.BACKUP_INTERVAL_HOURS)
|
|
||||||
scheduler.add_job(func=update_appointment_statuses, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
|
|
||||||
scheduler.add_job(func=create_return_reminders, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
|
|
||||||
scheduler.add_job(func=cleanup_expired_trial_tenants, trigger="interval", hours=1)
|
|
||||||
|
|
||||||
# 5. Add a heartbeat job to keep the lock alive
|
|
||||||
def renew_lock_heartbeat():
|
|
||||||
try:
|
|
||||||
hb_client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
|
||||||
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)}}
|
|
||||||
)
|
|
||||||
hb_client.close()
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"Scheduler heartbeat failed: {e}")
|
|
||||||
|
|
||||||
# Run heartbeat every 2 minutes (safely below the 5-minute timeout)
|
|
||||||
scheduler.add_job(func=renew_lock_heartbeat, trigger="interval", minutes=2)
|
|
||||||
|
|
||||||
scheduler.start()
|
|
||||||
_scheduler_initialized = True
|
|
||||||
app.logger.info(f"Scheduler started successfully (Worker ID: {_scheduler_worker_id})")
|
|
||||||
else:
|
|
||||||
app.logger.info("Scheduler skipped - another active worker holds the MongoDB lock")
|
|
||||||
|
|
||||||
client.close()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"Failed to initialize scheduler with MongoDB lock: {e}")
|
|
||||||
_scheduler_initialized = False
|
|
||||||
|
|
||||||
|
|
||||||
# Initialize scheduler on app startup
|
|
||||||
_initialize_scheduler()
|
|
||||||
|
|
||||||
|
|
||||||
def _shutdown_scheduler():
|
|
||||||
"""Gracefully shut down the scheduler and release the MongoDB lock."""
|
|
||||||
global _scheduler_initialized, _scheduler_worker_id
|
|
||||||
if getattr(cfg, 'SCHEDULER_ENABLED', False) and _scheduler_initialized:
|
|
||||||
try:
|
|
||||||
scheduler.shutdown(wait=False)
|
|
||||||
|
|
||||||
# Release the lock so a newly spawned worker can immediately take over
|
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
|
||||||
db = client[MONGODB_DB]
|
|
||||||
db['system_locks'].update_one(
|
|
||||||
{'_id': 'scheduler_lock', 'worker_id': _scheduler_worker_id},
|
|
||||||
{'$set': {'locked_at': datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)}}
|
|
||||||
)
|
|
||||||
client.close()
|
|
||||||
app.logger.info(f"Scheduler shut down and lock released (Worker ID: {_scheduler_worker_id})")
|
|
||||||
except Exception as e:
|
|
||||||
app.logger.error(f"Error during scheduler shutdown: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
try:
|
||||||
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
@@ -1811,13 +1682,137 @@ def create_return_reminders():
|
|||||||
f"Dies ist ein automatischer Bericht über deinen Status: <b>Dein Konto wurde soeben für neue Reservierungen gesperrt.</b><br><br>"
|
f"Dies ist ein automatischer Bericht über deinen Status: <b>Dein Konto wurde soeben für neue Reservierungen gesperrt.</b><br><br>"
|
||||||
f"Die Sperre wird erst aufgehoben, sobald der Artikel im System als zurückgegeben gemeldet wurde.")
|
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")
|
send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
|
||||||
app.logger.warning(f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.")
|
app.logger.warning(
|
||||||
|
f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.")
|
||||||
|
|
||||||
client.close()
|
client.close()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}")
|
app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}")
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
if updated_count > 0:
|
||||||
|
app.logger.warning(
|
||||||
|
f"Appointment status update finished: {updated_count} changed ({activated_count} active, {completed_count} completed)"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Automatic appointment status update failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize scheduler instances
|
||||||
|
scheduler = BackgroundScheduler()
|
||||||
|
_scheduler_initialized = False
|
||||||
|
_scheduler_worker_id = None # Tracks the unique ID of the worker holding the lock
|
||||||
|
|
||||||
|
|
||||||
|
def _initialize_scheduler():
|
||||||
|
"""Initialize the background scheduler safely using an atomic MongoDB lock."""
|
||||||
|
global _scheduler_initialized, _scheduler_worker_id
|
||||||
|
if _scheduler_initialized or not getattr(cfg, 'SCHEDULER_ENABLED', False):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
|
db = client[MONGODB_DB]
|
||||||
|
locks_col = db['system_locks']
|
||||||
|
|
||||||
|
# 1. Generate a unique ID for this specific worker process
|
||||||
|
_scheduler_worker_id = str(uuid.uuid4())
|
||||||
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
|
|
||||||
|
# 2. Ensure the lock document exists (initialize if missing)
|
||||||
|
try:
|
||||||
|
locks_col.insert_one({
|
||||||
|
'_id': 'scheduler_lock',
|
||||||
|
'locked_at': datetime.datetime.min.replace(tzinfo=datetime.timezone.utc),
|
||||||
|
'worker_id': None
|
||||||
|
})
|
||||||
|
except DuplicateKeyError:
|
||||||
|
pass # Document already exists, which is expected
|
||||||
|
|
||||||
|
# 3. Try to acquire the lock atomically
|
||||||
|
# We only acquire if the current lock is older than 5 minutes (stale/crashed worker)
|
||||||
|
# or if it was explicitly released (1970 init date)
|
||||||
|
lock_timeout = now - datetime.timedelta(minutes=5)
|
||||||
|
|
||||||
|
acquired = locks_col.find_one_and_update(
|
||||||
|
{
|
||||||
|
'_id': 'scheduler_lock',
|
||||||
|
'locked_at': {'$lt': lock_timeout}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'$set': {
|
||||||
|
'locked_at': now,
|
||||||
|
'worker_id': _scheduler_worker_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if acquired is not None:
|
||||||
|
# 4. Lock acquired successfully - this is the master worker
|
||||||
|
scheduler.add_job(func=create_daily_backup, trigger="interval", hours=cfg.BACKUP_INTERVAL_HOURS)
|
||||||
|
scheduler.add_job(func=update_appointment_statuses, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
|
||||||
|
scheduler.add_job(func=create_return_reminders, trigger="interval", minutes=cfg.SCHEDULER_INTERVAL_MIN)
|
||||||
|
scheduler.add_job(func=cleanup_expired_trial_tenants, trigger="interval", hours=1)
|
||||||
|
|
||||||
|
# 5. Add a heartbeat job to keep the lock alive
|
||||||
|
def renew_lock_heartbeat():
|
||||||
|
try:
|
||||||
|
hb_client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
|
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(ZoneInfo("Europe/Berlin"))}}
|
||||||
|
)
|
||||||
|
hb_client.close()
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Scheduler heartbeat failed: {e}")
|
||||||
|
|
||||||
|
# Run heartbeat every 2 minutes (safely below the 5-minute timeout)
|
||||||
|
scheduler.add_job(func=renew_lock_heartbeat, trigger="interval", minutes=2)
|
||||||
|
|
||||||
|
scheduler.start()
|
||||||
|
_scheduler_initialized = True
|
||||||
|
app.logger.info(f"Scheduler started successfully (Worker ID: {_scheduler_worker_id})")
|
||||||
|
else:
|
||||||
|
app.logger.info("Scheduler skipped - another active worker holds the MongoDB lock")
|
||||||
|
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Failed to initialize scheduler with MongoDB lock: {e}")
|
||||||
|
_scheduler_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize scheduler on app startup
|
||||||
|
_initialize_scheduler()
|
||||||
|
|
||||||
|
|
||||||
|
def _shutdown_scheduler():
|
||||||
|
"""Gracefully shut down the scheduler and release the MongoDB lock."""
|
||||||
|
global _scheduler_initialized, _scheduler_worker_id
|
||||||
|
if getattr(cfg, 'SCHEDULER_ENABLED', False) and _scheduler_initialized:
|
||||||
|
try:
|
||||||
|
scheduler.shutdown(wait=False)
|
||||||
|
|
||||||
|
# Release the lock so a newly spawned worker can immediately take over
|
||||||
|
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
|
||||||
|
db = client[MONGODB_DB]
|
||||||
|
db['system_locks'].update_one(
|
||||||
|
{'_id': 'scheduler_lock', 'worker_id': _scheduler_worker_id},
|
||||||
|
{'$set': {'locked_at': datetime.datetime.min.replace(tzinfo=datetime.timezone.utc)}}
|
||||||
|
)
|
||||||
|
client.close()
|
||||||
|
app.logger.info(f"Scheduler shut down and lock released (Worker ID: {_scheduler_worker_id})")
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Error during scheduler shutdown: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
atexit.register(_shutdown_scheduler)
|
||||||
|
|
||||||
|
|
||||||
"""-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """
|
"""-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """
|
||||||
|
|
||||||
def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB):
|
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({
|
student_cards_col.insert_one({
|
||||||
'AusweisId': row['ausweis_id'],
|
'AusweisId': row['ausweis_id'],
|
||||||
'StandardAusleihdauer': int(row['default_borrow_days']),
|
'StandardAusleihdauer': int(row['default_borrow_days']),
|
||||||
'Erstellt': datetime.datetime.now(),
|
'Erstellt': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
**encrypted_payload,
|
**encrypted_payload,
|
||||||
})
|
})
|
||||||
created_total += 1
|
created_total += 1
|
||||||
@@ -3635,10 +3630,246 @@ def library_loans_admin():
|
|||||||
client.close()
|
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')
|
@app.route('/test_mahnungen')
|
||||||
def test_mahnungen():
|
def test_mahnungen():
|
||||||
# Testdaten (Mock-Daten), um alle if/else Bedingungen im HTML zu testen
|
# 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",
|
"user": "Max Mustermann",
|
||||||
"klasse": "10A",
|
"klasse": "10A",
|
||||||
@@ -3669,7 +3900,7 @@ def test_mahnungen():
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Render das Template und übergebe die Testdaten
|
# 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')
|
@app.route('/mahnungen_admin')
|
||||||
@@ -3708,7 +3939,7 @@ def mahnungen_admin():
|
|||||||
except Exception:
|
except Exception:
|
||||||
return val
|
return val
|
||||||
|
|
||||||
current_time = datetime.datetime.now(datetime.timezone.utc)
|
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
client = None
|
client = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -4119,7 +4350,7 @@ def api_library_scan_action():
|
|||||||
|
|
||||||
item_id = str(item_doc['_id'])
|
item_id = str(item_doc['_id'])
|
||||||
borrower_name = card_doc.get('SchülerName') or f"Ausweis {student_card_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):
|
if item_doc.get('Verfuegbar', True):
|
||||||
borrow_duration_days = None
|
borrow_duration_days = None
|
||||||
@@ -4142,9 +4373,8 @@ def api_library_scan_action():
|
|||||||
card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS
|
card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS
|
||||||
borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_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)
|
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(
|
_append_audit_event_standalone(
|
||||||
event_type='ausleihung_borrowed',
|
event_type='ausleihung_borrowed',
|
||||||
@@ -4406,7 +4636,7 @@ def api_library_item_update(item_id):
|
|||||||
'Autor': author,
|
'Autor': author,
|
||||||
'Code_4': code_4,
|
'Code_4': code_4,
|
||||||
'ItemType': media_type,
|
'ItemType': media_type,
|
||||||
'LastUpdated': datetime.datetime.now()
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if normalized_isbn:
|
if normalized_isbn:
|
||||||
@@ -4600,7 +4830,7 @@ def student_cards_admin():
|
|||||||
{'$set': {
|
{'$set': {
|
||||||
'AusweisId': ausweis_id,
|
'AusweisId': ausweis_id,
|
||||||
'StandardAusleihdauer': int(default_borrow_days),
|
'StandardAusleihdauer': int(default_borrow_days),
|
||||||
'Aktualisiert': datetime.datetime.now(),
|
'Aktualisiert': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
**encrypted_payload
|
**encrypted_payload
|
||||||
}}
|
}}
|
||||||
)
|
)
|
||||||
@@ -4639,7 +4869,7 @@ def student_cards_admin():
|
|||||||
student_cards.insert_one({
|
student_cards.insert_one({
|
||||||
'AusweisId': ausweis_id,
|
'AusweisId': ausweis_id,
|
||||||
'StandardAusleihdauer': int(default_borrow_days),
|
'StandardAusleihdauer': int(default_borrow_days),
|
||||||
'Erstellt': datetime.datetime.now(),
|
'Erstellt': datetime.datetime.now(ZoneInfo("Europe/Berlin")),
|
||||||
**encrypted_payload,
|
**encrypted_payload,
|
||||||
})
|
})
|
||||||
flash('Neuer Ausweis wurde hinzugefügt.', 'success')
|
flash('Neuer Ausweis wurde hinzugefügt.', 'success')
|
||||||
@@ -4696,7 +4926,7 @@ def student_cards_print():
|
|||||||
return render_template(
|
return render_template(
|
||||||
'student_cards_print.html',
|
'student_cards_print.html',
|
||||||
student_cards=all_cards,
|
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(
|
return render_template(
|
||||||
'student_card_barcode_print.html',
|
'student_card_barcode_print.html',
|
||||||
student_cards=all_cards,
|
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')
|
download_link=url_for('student_card_barcode_download')
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4926,7 +5156,7 @@ def student_card_barcode_download():
|
|||||||
pdf_buffer,
|
pdf_buffer,
|
||||||
mimetype='application/pdf',
|
mimetype='application/pdf',
|
||||||
as_attachment=True,
|
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:
|
except Exception as e:
|
||||||
app.logger.error(f"Error occurred while generating PDF for card {card['AusweisId']}: {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()
|
c.save()
|
||||||
pdf_buffer.seek(0)
|
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(
|
return send_file(
|
||||||
pdf_buffer,
|
pdf_buffer,
|
||||||
@@ -6395,7 +6625,7 @@ def upload_item():
|
|||||||
|
|
||||||
def _soft_delete_item_groups(db, root_item_ids, username):
|
def _soft_delete_item_groups(db, root_item_ids, username):
|
||||||
"""Soft-delete one or more item groups and their borrow records."""
|
"""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 = []
|
unique_group_item_ids = []
|
||||||
seen_ids = set()
|
seen_ids = set()
|
||||||
|
|
||||||
@@ -6916,7 +7146,7 @@ def update_group():
|
|||||||
|
|
||||||
# 1. Shared Fields (Group Logic)
|
# 1. Shared Fields (Group Logic)
|
||||||
# These apply to every item in the group
|
# 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 (
|
for source_key, target_key in (
|
||||||
('name', 'Name'),
|
('name', 'Name'),
|
||||||
('ort', 'Ort'),
|
('ort', 'Ort'),
|
||||||
@@ -6991,7 +7221,7 @@ def report_damage(id):
|
|||||||
if not item_doc:
|
if not item_doc:
|
||||||
return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404
|
return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
damage_entry = {
|
damage_entry = {
|
||||||
'description': description,
|
'description': description,
|
||||||
'reported_by': session['username'],
|
'reported_by': session['username'],
|
||||||
@@ -7095,7 +7325,7 @@ def mark_damage_repaired(id):
|
|||||||
return jsonify({'success': False, 'message': 'Keine offenen Schäden vorhanden.'}), 400
|
return jsonify({'success': False, 'message': 'Keine offenen Schäden vorhanden.'}), 400
|
||||||
|
|
||||||
active_borrow = ausleihungen_col.find_one({'Item': str(id), 'Status': 'active'}, {'_id': 1})
|
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 = {
|
repair_entry = {
|
||||||
'repaired_by': session['username'],
|
'repaired_by': session['username'],
|
||||||
'repaired_at': now,
|
'repaired_at': now,
|
||||||
@@ -7272,7 +7502,7 @@ def ausleihen(id):
|
|||||||
card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS
|
card_default = cfg.STUDENT_DEFAULT_BORROW_DAYS
|
||||||
borrow_duration_days = max(1, min(card_default, cfg.STUDENT_MAX_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
|
end_date = None
|
||||||
if borrow_duration_days:
|
if borrow_duration_days:
|
||||||
end_date = start_date + datetime.timedelta(days=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
|
# Before borrowing, block if there's a conflicting planned booking
|
||||||
try:
|
try:
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
# Fetch planned bookings for this item from DB
|
# Fetch planned bookings for this item from DB
|
||||||
planned = au.get_planned_ausleihungen()
|
planned = au.get_planned_ausleihungen()
|
||||||
# Count relevant upcoming planned bookings for today or ongoing
|
# Count relevant upcoming planned bookings for today or ongoing
|
||||||
@@ -7422,12 +7652,12 @@ def ausleihen(id):
|
|||||||
return redirect(url_for(redirect_target))
|
return redirect(url_for(redirect_target))
|
||||||
|
|
||||||
# If we reach here, we can borrow the requested number of exemplars
|
# 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 the item doesn't use exemplars (single item)
|
||||||
if total_exemplare <= 1:
|
if total_exemplare <= 1:
|
||||||
it.update_item_status(id, False, effective_borrower)
|
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)
|
au.add_ausleihung(id, effective_borrower, start_date, end_date=end_date)
|
||||||
_append_audit_event_standalone(
|
_append_audit_event_standalone(
|
||||||
event_type='ausleihung_returned',
|
event_type='ausleihung_returned',
|
||||||
@@ -7470,7 +7700,7 @@ def ausleihen(id):
|
|||||||
it.update_item_status(id, False, username)
|
it.update_item_status(id, False, username)
|
||||||
|
|
||||||
# Create ausleihung records for each borrowed exemplar
|
# 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:
|
for exemplar in new_borrowed_exemplars:
|
||||||
exemplar_id = f"{id}_{exemplar['number']}"
|
exemplar_id = f"{id}_{exemplar['number']}"
|
||||||
au.add_ausleihung(exemplar_id, effective_borrower, start_date, end_date=end_date, exemplar_data={
|
au.add_ausleihung(exemplar_id, effective_borrower, start_date, end_date=end_date, exemplar_data={
|
||||||
@@ -7532,7 +7762,7 @@ def zurueckgeben(id):
|
|||||||
'Status': 'active'
|
'Status': 'active'
|
||||||
})
|
})
|
||||||
|
|
||||||
end_date = datetime.datetime.now()
|
end_date = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
original_user = item.get('User', username)
|
original_user = item.get('User', username)
|
||||||
|
|
||||||
updated_count = 0
|
updated_count = 0
|
||||||
@@ -7544,7 +7774,7 @@ def zurueckgeben(id):
|
|||||||
{'$set': {
|
{'$set': {
|
||||||
'Status': 'completed',
|
'Status': 'completed',
|
||||||
'End': end_date,
|
'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
|
# Also include current availability if checking today and item is borrowed now
|
||||||
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
|
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
|
||||||
if item_doc and not item_doc.get('Verfuegbar', True):
|
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():
|
if req_start.date() == now.date():
|
||||||
conflicts.append({'status': 'active', 'user': item_doc.get('User'), 'start': None, 'end': None, 'period': None, 'id': None})
|
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']
|
items_col = db['items']
|
||||||
users_col = db['users'] # Direkter Zugriff auf die User-Collection
|
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
|
# 1. Aktive Ausleihen abschließen
|
||||||
ausleihungen.update_many(
|
ausleihungen.update_many(
|
||||||
@@ -8906,7 +9136,7 @@ def admin_reset_borrowing(borrow_id):
|
|||||||
item_id = rec.get('Item')
|
item_id = rec.get('Item')
|
||||||
user = rec.get('User')
|
user = rec.get('User')
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
if status == 'active':
|
if status == 'active':
|
||||||
ausleihungen.update_one({'_id': rec['_id']}, {'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}})
|
ausleihungen.update_one({'_id': rec['_id']}, {'$set': {'Status': 'completed', 'End': now, 'LastUpdated': now}})
|
||||||
# Free the item
|
# Free the item
|
||||||
@@ -9004,7 +9234,7 @@ def admin_create_invoice(borrow_id):
|
|||||||
|
|
||||||
mark_destroyed = request.form.get('mark_destroyed') == 'on'
|
mark_destroyed = request.form.get('mark_destroyed') == 'on'
|
||||||
close_borrowing = request.form.get('close_borrowing') == '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 {}
|
existing_invoice = borrow_doc.get('InvoiceData') or {}
|
||||||
if existing_invoice.get('invoice_number'):
|
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')
|
flash('Rechnung ist bereits als bezahlt markiert.', 'info')
|
||||||
return redirect(url_for('admin_borrowings'))
|
return redirect(url_for('admin_borrowings'))
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
result = ausleihungen.update_one(
|
result = ausleihungen.update_one(
|
||||||
{'_id': borrow_doc['_id']},
|
{'_id': borrow_doc['_id']},
|
||||||
{
|
{
|
||||||
@@ -9252,7 +9482,7 @@ def admin_pay_invoice_extended(borrow_id):
|
|||||||
flash('Für diese Ausleihung existiert keine Rechnung.', 'warning')
|
flash('Für diese Ausleihung existiert keine Rechnung.', 'warning')
|
||||||
return redirect(request.referrer or url_for('library_loans_admin'))
|
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}
|
update_fields = {'LastUpdated': now}
|
||||||
|
|
||||||
if invoice_data.get('paid') is not True:
|
if invoice_data.get('paid') is not True:
|
||||||
@@ -9322,7 +9552,7 @@ def resolve_repaired_item(item_id):
|
|||||||
flash('Element nicht gefunden.', 'error')
|
flash('Element nicht gefunden.', 'error')
|
||||||
return redirect(request.referrer or url_for('library_loans_admin'))
|
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 []
|
open_reports = item_doc.get('DamageReports', []) or []
|
||||||
|
|
||||||
item_update = {
|
item_update = {
|
||||||
@@ -9459,7 +9689,7 @@ def admin_add_invoice_correction(borrow_id):
|
|||||||
flash('Ungültiger Korrekturbetrag.', 'error')
|
flash('Ungültiger Korrekturbetrag.', 'error')
|
||||||
return redirect(url_for('admin_borrowings'))
|
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_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}"
|
||||||
|
|
||||||
correction_entry = {
|
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."
|
return False, "Item nicht in der Datenbank gefunden."
|
||||||
|
|
||||||
series_group_id = item.get('SeriesGroupId')
|
series_group_id = item.get('SeriesGroupId')
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
||||||
|
|
||||||
legacy_damage_unset = {
|
legacy_damage_unset = {
|
||||||
'has_damage': "",
|
'has_damage': "",
|
||||||
@@ -10060,7 +10290,7 @@ def admin_anonymize_names():
|
|||||||
|
|
||||||
result = student_cards_col.update_one(
|
result = student_cards_col.update_one(
|
||||||
{'_id': card_doc['_id']},
|
{'_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:
|
if result.modified_count > 0:
|
||||||
cards_updated += 1
|
cards_updated += 1
|
||||||
@@ -11174,7 +11404,7 @@ def mark_notification_read(notification_id):
|
|||||||
{'_id': ObjectId(notification_id)},
|
{'_id': ObjectId(notification_id)},
|
||||||
{
|
{
|
||||||
'$addToSet': {'ReadBy': username},
|
'$addToSet': {'ReadBy': username},
|
||||||
'$set': {'UpdatedAt': datetime.datetime.now()}
|
'$set': {'UpdatedAt': datetime.datetime.now(ZoneInfo("Europe/Berlin"))}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if result.modified_count > 0:
|
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
|
return jsonify({'success': False, 'message': f'Fehler beim Prüfen der Verfügbarkeit'}), 500
|
||||||
|
|
||||||
# Check if the appointment should already be active
|
# 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'
|
initial_status = 'active' if start_datetime <= now else 'planned'
|
||||||
|
|
||||||
# Create the appointment
|
# Create the appointment
|
||||||
|
|||||||
Reference in New Issue
Block a user