dffcfdcba7
Änderungen Geplante Inventar-Ausleihen werden zum Startzeitpunkt automatisch aktiviert, wenn ein Exemplar verfügbar ist. Bei fehlender Verfügbarkeit bleibt die Reservierung geplant. Der reservierende Benutzer und der aktuelle Ausleiher erhalten jeweils eine Benachrichtigung. Die automatische Aktivierung wird beim nächsten Scheduler-Lauf erneut versucht, sobald der Gegenstand zurückgegeben wurde. Das geplante Ende beendet eine aktive Ausleihe nicht mehr automatisch. Nach Ablauf erscheint bei „Meine Ausleihungen“ ein deutlich roter Hinweis: RÜCKGABE ERFORDERLICH roter, hervorgehobener Button Rückgabe bestätigen Eine Ausleihe wird erst nach dem expliziten Bestätigen der Rückgabe auf completed gesetzt und wieder verfügbar gemacht. Rückgabeaktionen unterstützen nun auch Datensätze, bei denen die Item-ID als ObjectId gespeichert ist. Die Dokumentation des Ausleih-Workflows wurde aktualisiert. Geänderte Dateien: Web/app.py Web/modules/database/ausleihung.py Web/templates/my_borrowed_items.html Legal/TEST_AUSLEIHUNG.md Validiert mit: Python-Syntaxprüfung erfolgreich git diff --check erfolgreich Im Repository wurden keine ausführbaren Python-Testdateien gefunden
894 lines
32 KiB
Python
Executable File
894 lines
32 KiB
Python
Executable File
"""
|
|
Ausleihungssystem (Borrowing System)
|
|
====================================
|
|
|
|
Dieses Modul verwaltet sämtliche Ausleihungen im Inventarsystem.
|
|
Es bietet alle Funktionen, um Ausleihungen zu planen, zu aktivieren,
|
|
zu beenden und zu stornieren.
|
|
|
|
Hauptfunktionen:
|
|
- Erstellen neuer Ausleihungen (geplant oder sofort aktiv)
|
|
- Aktualisieren von Ausleihungsdaten
|
|
- Abschließen von Ausleihungen (Rückgabe)
|
|
- Suchen und Abrufen von Ausleihungen nach verschiedenen Kriterien
|
|
- Verwaltung des Ausleihungs-Lebenszyklus
|
|
|
|
Sammlungsstruktur:
|
|
- ausleihungen: Speichert alle Ausleihungsdatensätze mit ihrem Status
|
|
- Status-Werte: 'planned' (geplant), 'active' (aktiv), 'completed' (nach bestätigter Rückgabe), 'cancelled' (storniert)
|
|
- Das geplante Ende beendet eine aktive Ausleihe nicht automatisch; es erzeugt einen Rückgabehinweis.
|
|
"""
|
|
'''
|
|
Copyright 2025-2026 AIIrondev
|
|
|
|
Licensed under the Inventarsystem EULA (Endbenutzer-Lizenzvertrag).
|
|
See Legal/LICENSE for the full license text.
|
|
Unauthorized commercial use, SaaS hosting, or removal of branding is prohibited.
|
|
For commercial licensing inquiries: https://github.com/AIIrondev
|
|
'''
|
|
from bson.objectid import ObjectId, InvalidId
|
|
import datetime
|
|
from dateutil import parser
|
|
import os
|
|
import json
|
|
import Web.modules.database.settings as cfg
|
|
from Web.modules.database.settings import MongoClient
|
|
import Web.modules.inventarsystem.data_protection as dp
|
|
import Web.modules.database.user as us
|
|
from zoneinfo import ZoneInfo
|
|
|
|
|
|
def _get_client():
|
|
"""Hilfsfunktion für die Erstellung einer MongoClient-Instanz."""
|
|
return MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
# Add this helper function after imports
|
|
def ensure_timezone_aware(dt):
|
|
"""Return a timezone-aware datetime, treating naive DB values as UTC."""
|
|
if dt is None:
|
|
return None
|
|
if dt.tzinfo is None:
|
|
# PyMongo returns BSON datetimes as naive UTC unless tz_aware is enabled.
|
|
return dt.replace(tzinfo=datetime.timezone.utc)
|
|
return dt
|
|
|
|
def get_current_status(ausleihung, log_changes=False, user=None):
|
|
"""
|
|
Ermittelt den aktuellen Status einer Ausleihung basierend auf den Zeitstempeln
|
|
und dem gespeicherten Status. Diese Funktion berücksichtigt das aktuelle Datum
|
|
und aktualisiert den Status entsprechend dem realen Zustand.
|
|
|
|
Status-Werte:
|
|
- 'planned': Eine zukünftige Ausleihung, die noch nicht begonnen hat
|
|
- 'active': Eine aktive Ausleihung, die gerade läuft
|
|
- 'completed': Eine beendete Ausleihung
|
|
- 'cancelled': Eine stornierte Ausleihung
|
|
|
|
Args:
|
|
ausleihung (dict): Der Ausleihungsdatensatz
|
|
log_changes (bool): Ob Statusänderungen protokolliert werden sollen
|
|
user (str): Der Benutzer, der die Prüfung durchführt (für Logs)
|
|
|
|
Returns:
|
|
str: Der aktuelle Status ('planned', 'active', 'completed', 'cancelled')
|
|
"""
|
|
# Speichern Sie den ursprünglichen Status für Logging-Zwecke
|
|
original_status = ausleihung.get('Status', 'unknown')
|
|
|
|
# Bei stornierten Ausleihungen bleibt der Status immer storniert
|
|
if original_status == 'cancelled':
|
|
return 'cancelled'
|
|
|
|
# Wenn die Ausleihung bereits abgeschlossen ist, bleibt sie es
|
|
if original_status == 'completed':
|
|
return 'completed'
|
|
|
|
current_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
start_time = ensure_timezone_aware(ausleihung.get('Start'))
|
|
end_time = ensure_timezone_aware(ausleihung.get('End'))
|
|
|
|
# Wenn kein Startdatum vorhanden ist, Status auf 'planned' setzen
|
|
if not start_time:
|
|
new_status = 'planned'
|
|
# Wenn die aktuelle Zeit vor dem Startdatum liegt, ist die Ausleihung geplant
|
|
elif current_time < start_time:
|
|
# DEBUG: Log info wenn Booking noch lange in der Zukunft ist
|
|
time_until_start = (start_time - current_time).total_seconds()
|
|
if time_until_start > 3600: # Mehr als 1 Stunde entfernt
|
|
ausleihung_id = str(ausleihung.get('_id', 'unknown'))[:12]
|
|
print(f"[DEBUG] Ausleihe {ausleihung_id} startet in {time_until_start/3600:.1f} Stunden ({start_time}), noch geplant")
|
|
new_status = 'planned'
|
|
# Eine aktive Ausleihe bleibt aktiv, bis die Rückgabe ausdrücklich bestätigt
|
|
# wurde. Das geplante Ende löst nur einen Rückgabehinweis aus.
|
|
else:
|
|
new_status = 'active'
|
|
|
|
# Protokollieren Sie Statusänderungen, wenn aktiviert und eine Änderung stattgefunden hat
|
|
if log_changes and new_status != original_status and '_id' in ausleihung:
|
|
try:
|
|
# Importieren Sie das Modul nur bei Bedarf, um zirkuläre Importe zu vermeiden
|
|
import Web.modules.log.ausleihung_log as ausleihung_log
|
|
ausleihung_log.log_status_change(
|
|
str(ausleihung['_id']),
|
|
original_status,
|
|
new_status,
|
|
dp.encrypt_text(user)
|
|
)
|
|
except Exception as e:
|
|
print(f"Fehler beim Protokollieren der Statusänderung: {e}")
|
|
|
|
return new_status
|
|
|
|
def create_backup_database():
|
|
"""
|
|
Erstellt eine Sicherungskopie der Ausleihungsdatenbank.
|
|
|
|
Returns:
|
|
bool: True wenn Backup erfolgreich erstellt wurde, sonst False
|
|
"""
|
|
try:
|
|
# Verbindung zur Datenbank herstellen
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
# Backup-Verzeichnis erstellen, falls es nicht existiert
|
|
backup_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'backups')
|
|
if not os.path.exists(backup_dir):
|
|
os.makedirs(backup_dir)
|
|
|
|
# Aktuelles Datum für den Dateinamen
|
|
current_date = datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d_%H-%M-%S')
|
|
backup_file = os.path.join(backup_dir, f'ausleihungen_backup_{current_date}.json')
|
|
|
|
# Ausleihungen abrufen und als JSON speichern
|
|
all_ausleihungen = list(ausleihungen.find({}))
|
|
|
|
# ObjectId in String umwandeln für JSON-Serialisierung
|
|
for ausleihung in all_ausleihungen:
|
|
ausleihung['_id'] = str(ausleihung['_id'])
|
|
if 'Start' in ausleihung and ausleihung['Start']:
|
|
ausleihung['Start'] = ausleihung['Start'].isoformat()
|
|
if 'End' in ausleihung and ausleihung['End']:
|
|
ausleihung['End'] = ausleihung['End'].isoformat()
|
|
if 'LastUpdated' in ausleihung and ausleihung['LastUpdated']:
|
|
ausleihung['LastUpdated'] = ausleihung['LastUpdated'].isoformat()
|
|
|
|
with open(backup_file, 'w', encoding='utf-8') as f:
|
|
json.dump(all_ausleihungen, f, ensure_ascii=False, indent=4)
|
|
|
|
# Log-Eintrag erstellen
|
|
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')
|
|
if not os.path.exists(log_dir):
|
|
os.makedirs(log_dir)
|
|
|
|
log_file = os.path.join(log_dir, 'ausleihungen_backup.log')
|
|
with open(log_file, 'a', encoding='utf-8') as f:
|
|
f.write(f"{current_date}: Backup erstellt: {backup_file}, {len(all_ausleihungen)} Einträge\n")
|
|
|
|
client.close()
|
|
return True
|
|
except Exception as e:
|
|
# Fehler protokollieren
|
|
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs')
|
|
if not os.path.exists(log_dir):
|
|
os.makedirs(log_dir)
|
|
|
|
log_file = os.path.join(log_dir, 'ausleihungen_error.log')
|
|
with open(log_file, 'a', encoding='utf-8') as f:
|
|
f.write(f"{datetime.datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d_%H-%M-%S')}: Backup-Fehler: {str(e)}\n")
|
|
|
|
print(f"Fehler beim Erstellen des Backups: {e}")
|
|
return False
|
|
|
|
|
|
# === AUSLEIHUNG MANAGEMENT ===
|
|
|
|
def add_ausleihung(item_id, user_id, start_date, end_date=None, notes="", status="active", period=None, exemplar_data=None, due_date=None):
|
|
"""
|
|
Add a new borrowing record for an item.
|
|
|
|
Args:
|
|
item_id (str): ID of the item borrowed
|
|
user_id (str): ID of the borrower
|
|
start_date (datetime): Start date and time of the borrowing
|
|
end_date (datetime, optional): End date and time if already returned
|
|
notes (str, optional): Additional notes about the borrowing
|
|
status (str, optional): Status of the borrowing (active, completed, planned)
|
|
period (str, optional): School period for the borrowing
|
|
exemplar_data (dict, optional): Information about specific exemplar borrowed
|
|
|
|
Returns:
|
|
ObjectId: ID of the new borrowing record or None if failed
|
|
"""
|
|
try:
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
ausleihung = {
|
|
'Item': item_id,
|
|
'User': str(user_id),
|
|
'Start': start_date,
|
|
'Status': status
|
|
}
|
|
|
|
if end_date:
|
|
ausleihung['End'] = end_date
|
|
|
|
if notes:
|
|
ausleihung['Notes'] = notes
|
|
|
|
if period:
|
|
ausleihung['Period'] = period
|
|
|
|
if exemplar_data:
|
|
ausleihung['ExemplarData'] = exemplar_data
|
|
|
|
if due_date:
|
|
ausleihung['DueDate'] = due_date
|
|
|
|
result = ausleihungen.insert_one(ausleihung)
|
|
ausleihung_id = result.inserted_id
|
|
|
|
client.close()
|
|
return ausleihung_id
|
|
except Exception as e:
|
|
print(f"Error adding ausleihung: {e}")
|
|
return None
|
|
|
|
def update_ausleihung(id, item_id=None, user_id=None, start=None, end=None, notes=None, status=None, period=None):
|
|
try:
|
|
# ID-Validierung
|
|
try:
|
|
doc_id = ObjectId(id) if isinstance(id, str) else id
|
|
except InvalidId:
|
|
print(f"Ungültiges ObjectId-Format: {id}")
|
|
return False
|
|
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
update_data = {}
|
|
|
|
if item_id is not None:
|
|
update_data['Item'] = item_id
|
|
if user_id is not None:
|
|
update_data['User'] = str(user_id)
|
|
if start is not None:
|
|
update_data['Start'] = ensure_timezone_aware(start)
|
|
if end is not None:
|
|
update_data['End'] = ensure_timezone_aware(end)
|
|
if notes is not None:
|
|
update_data['Notes'] = notes
|
|
if status is not None:
|
|
update_data['Status'] = status
|
|
if period is not None:
|
|
update_data['Period'] = period
|
|
|
|
# Keine leeren Updates ausführen (falls nur None-Parameter übergeben wurden)
|
|
if not update_data:
|
|
client.close()
|
|
return True
|
|
|
|
# UTC Zeitstempel nutzen
|
|
update_data['LastUpdated'] = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
|
|
result = ausleihungen.update_one(
|
|
{'_id': doc_id},
|
|
{'$set': update_data}
|
|
)
|
|
|
|
client.close()
|
|
|
|
# Prüfen ob das Dokument überhaupt gefunden wurde (matched_count)
|
|
return result.matched_count > 0
|
|
|
|
except Exception as e:
|
|
print(f"Error updating ausleihung: {e}")
|
|
return False
|
|
|
|
def complete_ausleihung(id, end_time=None):
|
|
"""
|
|
Markiert eine Ausleihe als abgeschlossen, indem das Enddatum gesetzt
|
|
und der Status auf 'completed' geändert wird.
|
|
|
|
Args:
|
|
id (str): ID des abzuschließenden Ausleihungsdatensatzes
|
|
end_time (datetime, optional): Endzeitpunkt (Standard: aktuelle Zeit)
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, sonst False
|
|
"""
|
|
try:
|
|
if end_time is None:
|
|
end_time = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
item = db['items']
|
|
|
|
result = ausleihungen.update_one(
|
|
{'_id': ObjectId(id)},
|
|
{'$set': {
|
|
'End': end_time,
|
|
'Status': 'completed',
|
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
}}
|
|
)
|
|
|
|
item.update_one(
|
|
{'_id': ObjectId(id)},
|
|
{'$set': {
|
|
'Verfuegbar': True,
|
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
}}
|
|
)
|
|
|
|
client.close()
|
|
return result.modified_count > 0
|
|
except Exception as e:
|
|
# print(f"Error completing ausleihung: {e}") # Log the error
|
|
return False
|
|
|
|
|
|
def cancel_ausleihung(id):
|
|
"""
|
|
Storniert eine geplante Ausleihe durch Änderung des Status auf 'cancelled'.
|
|
|
|
Args:
|
|
id (str): ID der zu stornierenden Ausleihe
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, sonst False
|
|
"""
|
|
try:
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
# Mark the booking as cancelled
|
|
result = ausleihungen.update_one(
|
|
{'_id': ObjectId(id), 'Status': {'$in': ['planned', 'active']}},
|
|
{'$set': {
|
|
'Status': 'cancelled',
|
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
}}
|
|
)
|
|
|
|
client.close()
|
|
return result.modified_count > 0
|
|
except Exception as e:
|
|
# print(f"Error cancelling ausleihung: {e}") # Log the error
|
|
return False
|
|
|
|
|
|
def get_booking(id):
|
|
"""Compatibility wrapper for the booking route."""
|
|
return get_ausleihung(id)
|
|
|
|
|
|
def cancel_booking(id):
|
|
"""Compatibility wrapper for the booking route."""
|
|
return cancel_ausleihung(id)
|
|
|
|
|
|
def remove_ausleihung(id):
|
|
"""
|
|
Markiert einen Ausleihungsdatensatz als gelöscht (Soft-Delete).
|
|
|
|
Args:
|
|
id (str): ID des zu entfernenden Ausleihungsdatensatzes
|
|
|
|
Returns:
|
|
bool: True bei Erfolg, sonst False
|
|
"""
|
|
try:
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
now = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
result = ausleihungen.update_one(
|
|
{'_id': ObjectId(id), 'Status': {'$ne': 'deleted'}},
|
|
{'$set': {
|
|
'Status': 'deleted',
|
|
'DeletedAt': now,
|
|
'LastUpdated': now,
|
|
}}
|
|
)
|
|
client.close()
|
|
return result.modified_count > 0
|
|
except Exception as e:
|
|
# print(f"Error removing ausleihung: {e}") # Log the error
|
|
return False
|
|
|
|
|
|
# === AUSLEIHUNG RETRIEVAL ===
|
|
|
|
def get_ausleihung(id):
|
|
"""
|
|
Ruft einen bestimmten Ausleihungsdatensatz anhand seiner ID ab.
|
|
|
|
Args:
|
|
id (str): ID des abzurufenden Ausleihungsdatensatzes
|
|
|
|
Returns:
|
|
dict: Der Ausleihungsdatensatz oder None, wenn nicht gefunden
|
|
"""
|
|
try:
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
ausleihung = ausleihungen.find_one({'_id': ObjectId(id)})
|
|
client.close()
|
|
return ausleihung
|
|
except Exception as e:
|
|
# print(f"Error retrieving ausleihung: {e}") # Log the error
|
|
return None
|
|
|
|
|
|
def get_ausleihungen(status=None, start=None, end=None, date_filter='overlap'):
|
|
try:
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
db = client[cfg.MONGODB_DB]
|
|
collection = db['ausleihungen']
|
|
|
|
query = {'Status': {'$ne': 'deleted'}}
|
|
|
|
# Status-Filter
|
|
if status is not None:
|
|
if isinstance(status, list):
|
|
allowed_status = [s for s in status if s != 'deleted']
|
|
query['Status'] = {'$in': allowed_status}
|
|
else:
|
|
query['Status'] = status if status != 'deleted' else '__blocked_deleted_status__'
|
|
|
|
# Datums-Parsing
|
|
if isinstance(start, str):
|
|
try:
|
|
start = parser.parse(start)
|
|
except Exception:
|
|
start = None
|
|
|
|
if isinstance(end, str):
|
|
try:
|
|
end = parser.parse(end)
|
|
except Exception:
|
|
end = None
|
|
|
|
# Datumsfilter-Logik
|
|
if start is not None and end is not None:
|
|
if date_filter == 'overlap':
|
|
query['$and'] = [
|
|
{'Start': {'$lte': end}},
|
|
{'$or': [
|
|
{'End': {'$gte': start}},
|
|
{'End': None}
|
|
]}
|
|
]
|
|
elif date_filter == 'start_in':
|
|
query['Start'] = {'$gte': start, '$lte': end}
|
|
elif date_filter == 'end_in':
|
|
query['End'] = {'$gte': start, '$lte': end}
|
|
elif date_filter == 'contained':
|
|
query['Start'] = {'$gte': start}
|
|
query['End'] = {'$exists': True, '$lte': end}
|
|
elif start is not None:
|
|
# Nur Startdatum angegeben -> alles was ab/nach start aktiv ist
|
|
query['$or'] = [{'End': {'$gte': start}}, {'End': None}]
|
|
elif end is not None:
|
|
# Nur Enddatum angegeben -> alles was vor end begonnen hat
|
|
query['Start'] = {'$lte': end}
|
|
|
|
results = list(collection.find(query))
|
|
client.close()
|
|
return results
|
|
except Exception as e:
|
|
# Sinnvolles Logging einbauen
|
|
return []
|
|
|
|
def get_active_ausleihungen(start=None, end=None):
|
|
"""
|
|
Ruft alle aktiven (laufenden) Ausleihungen ab.
|
|
|
|
Args:
|
|
start (str/datetime, optional): Startdatum für Datumsfilterung
|
|
end (str/datetime, optional): Enddatum für Datumsfilterung
|
|
|
|
Returns:
|
|
list: Liste aktiver Ausleihungsdatensätze
|
|
"""
|
|
return get_ausleihungen(status='active', start=start, end=end)
|
|
|
|
|
|
def get_planned_ausleihungen(start=None, end=None):
|
|
"""
|
|
Ruft alle geplanten Ausleihungen (Reservierungen) ab.
|
|
|
|
Args:
|
|
start (str/datetime, optional): Startdatum für Datumsfilterung
|
|
end (str/datetime, optional): Enddatum für Datumsfilterung
|
|
|
|
Returns:
|
|
list: Liste geplanter Ausleihungsdatensätze
|
|
"""
|
|
return get_ausleihungen(status='planned', start=start, end=end)
|
|
|
|
|
|
def get_completed_ausleihungen(start=None, end=None):
|
|
"""
|
|
Ruft alle abgeschlossenen Ausleihungen ab.
|
|
|
|
Args:
|
|
start (str/datetime, optional): Startdatum für Datumsfilterung
|
|
end (str/datetime, optional): Enddatum für Datumsfilterung
|
|
|
|
Returns:
|
|
list: Liste abgeschlossener Ausleihungsdatensätze
|
|
"""
|
|
return get_ausleihungen(status='completed', start=start, end=end)
|
|
|
|
|
|
def get_cancelled_ausleihungen(start=None, end=None):
|
|
"""
|
|
Ruft alle stornierten Ausleihungen ab.
|
|
|
|
Args:
|
|
start (str/datetime, optional): Startdatum für Datumsfilterung
|
|
end (str/datetime, optional): Enddatum für Datumsfilterung
|
|
|
|
Returns:
|
|
list: Liste stornierter Ausleihungsdatensätze
|
|
"""
|
|
return get_ausleihungen(status='cancelled', start=start, end=end)
|
|
|
|
|
|
# === SEARCH FUNCTIONS ===
|
|
|
|
def get_ausleihung_by_user(user_id, status=None, use_client_side_verification=True):
|
|
"""Ruft Ausleihungen für einen bestimmten Benutzer ab."""
|
|
try:
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
query = {'User': str(user_id), 'Status': {'$ne': 'deleted'}}
|
|
|
|
if not use_client_side_verification:
|
|
if status is not None:
|
|
if isinstance(status, list):
|
|
query['Status'] = {'$in': status}
|
|
else:
|
|
query['Status'] = status
|
|
else:
|
|
query['Status'] = {'$ne': 'cancelled'}
|
|
return list(ausleihungen.find(query))
|
|
|
|
all_ausleihungen = list(ausleihungen.find(query))
|
|
|
|
for record in all_ausleihungen:
|
|
current_status = get_current_status(record) if 'get_current_status' in globals() else record.get(
|
|
'Status')
|
|
record['VerifiedStatus'] = current_status
|
|
|
|
if status is None:
|
|
return all_ausleihungen
|
|
|
|
filtered_results = []
|
|
target_statuses = status if isinstance(status, list) else [status]
|
|
|
|
for record in all_ausleihungen:
|
|
v_status = record.get('VerifiedStatus')
|
|
if v_status in target_statuses and v_status != 'deleted':
|
|
filtered_results.append(record)
|
|
|
|
return filtered_results
|
|
except Exception as e:
|
|
print(f"Error retrieving ausleihungen for user {user_id}: {e}")
|
|
return []
|
|
|
|
|
|
def get_ausleihung_by_item(item_id, status=None, include_history=False):
|
|
"""Get most recent ausleihung record for a specific item."""
|
|
try:
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
query = {'Item': item_id, 'Status': {'$ne': 'deleted'}}
|
|
if status and not include_history:
|
|
query['Status'] = status
|
|
|
|
record = ausleihungen.find_one(query, sort=[('Start', -1)])
|
|
if record:
|
|
record['_id'] = str(record['_id'])
|
|
return record
|
|
except Exception as e:
|
|
print(f"Error getting ausleihung by item: {e}")
|
|
return None
|
|
|
|
|
|
def get_ausleihungen_by_date_range(start_date, end_date, status=None):
|
|
"""Ruft Ausleihungen ab, die in einem bestimmten Zeitraum aktiv waren."""
|
|
return get_ausleihungen(status=status, start=start_date, end=end_date)
|
|
|
|
|
|
def check_ausleihung_conflict(item_id, start_date, end_date, period=None):
|
|
"""Prüft, ob es Konflikte mit bestehenden Ausleihungen oder aktiven Ausleihen gibt."""
|
|
try:
|
|
start_date = ensure_timezone_aware(start_date)
|
|
end_date = ensure_timezone_aware(end_date)
|
|
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
booking_date = start_date.date()
|
|
all_bookings = list(ausleihungen.find({
|
|
'Item': item_id,
|
|
'Status': {'$in': ['planned', 'active']}
|
|
}))
|
|
|
|
if period is not None:
|
|
period_int = int(period)
|
|
for booking in all_bookings:
|
|
booking_start = ensure_timezone_aware(booking.get('Start'))
|
|
if booking_start and booking_start.date() == booking_date:
|
|
booking_period = booking.get('Period')
|
|
if booking_period is not None and int(booking_period) == period_int:
|
|
return True
|
|
|
|
for booking in all_bookings:
|
|
b_start = ensure_timezone_aware(booking.get('Start'))
|
|
if not b_start:
|
|
continue
|
|
b_end = ensure_timezone_aware(booking.get('End')) or (b_start + datetime.timedelta(hours=1))
|
|
|
|
# Simplified and standard time range collision check
|
|
if start_date < b_end and end_date > b_start:
|
|
return True
|
|
|
|
return False
|
|
except Exception as e:
|
|
print(f"Error checking booking conflicts: {e}")
|
|
return True # Return conflict on exception for safety
|
|
|
|
|
|
def check_booking_period_range_conflict(item_id, start_date, end_date, period=None, period_end=None):
|
|
"""Checks for conflicts with existing bookings, supporting period ranges."""
|
|
try:
|
|
start_date = ensure_timezone_aware(start_date)
|
|
end_date = ensure_timezone_aware(end_date)
|
|
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
booking_date = start_date.date()
|
|
all_bookings = list(ausleihungen.find({
|
|
'Item': item_id,
|
|
'Status': {'$in': ['planned', 'active']}
|
|
}))
|
|
|
|
if period is not None:
|
|
period_start = int(period)
|
|
periods_to_check = [period_start]
|
|
if period_end is not None:
|
|
periods_to_check = list(range(period_start, int(period_end) + 1))
|
|
|
|
for booking in all_bookings:
|
|
b_start = ensure_timezone_aware(booking.get('Start'))
|
|
if b_start and b_start.date() == booking_date:
|
|
booking_period = booking.get('Period')
|
|
try:
|
|
booking_period_int = int(booking_period) if booking_period is not None else None
|
|
except Exception:
|
|
booking_period_int = None
|
|
|
|
if booking_period_int in periods_to_check:
|
|
return True
|
|
|
|
for booking in all_bookings:
|
|
b_start = ensure_timezone_aware(booking.get('Start'))
|
|
if not b_start:
|
|
continue
|
|
b_end = ensure_timezone_aware(booking.get('End')) or (b_start + datetime.timedelta(hours=1))
|
|
|
|
if start_date < b_end and end_date > b_start:
|
|
return True
|
|
|
|
return False
|
|
except Exception as e:
|
|
print(f"Error checking booking range conflicts: {e}")
|
|
return True
|
|
|
|
|
|
def get_ausleihungen_starting_now(current_time):
|
|
"""Ruft Ausleihungen ab, die jetzt beginnen sollen."""
|
|
try:
|
|
current_time = ensure_timezone_aware(current_time)
|
|
start_time = current_time - datetime.timedelta(hours=3)
|
|
end_time = current_time + datetime.timedelta(hours=1)
|
|
today = current_time.date()
|
|
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
query = {
|
|
'Status': 'planned',
|
|
'$or': [
|
|
{'Start': {'$lte': end_time, '$gte': start_time}},
|
|
{
|
|
'Period': {'$exists': True},
|
|
'Start': {
|
|
'$gte': datetime.datetime.combine(today, datetime.time.min, tzinfo=datetime.timezone.utc),
|
|
'$lt': datetime.datetime.combine(today + datetime.timedelta(days=1), datetime.time.min,
|
|
tzinfo=datetime.timezone.utc)
|
|
}
|
|
}
|
|
]
|
|
}
|
|
return list(ausleihungen.find(query))
|
|
except Exception as e:
|
|
print(f"Error in get_ausleihungen_starting_now: {e}")
|
|
return []
|
|
|
|
|
|
def get_ausleihungen_ending_now(current_time):
|
|
"""Ruft Ausleihungen ab, die jetzt enden sollen."""
|
|
try:
|
|
current_time = ensure_timezone_aware(current_time)
|
|
start_time = current_time - datetime.timedelta(minutes=15)
|
|
end_time = current_time + datetime.timedelta(minutes=5)
|
|
today = current_time.date()
|
|
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
query = {
|
|
'Status': 'active',
|
|
'$or': [
|
|
{'End': {'$gte': start_time, '$lte': end_time}},
|
|
{
|
|
'Period': {'$exists': True},
|
|
'Start': {
|
|
'$gte': datetime.datetime.combine(today, datetime.time.min, tzinfo=datetime.timezone.utc),
|
|
'$lt': datetime.datetime.combine(today + datetime.timedelta(days=1), datetime.time.min,
|
|
tzinfo=datetime.timezone.utc)
|
|
}
|
|
}
|
|
]
|
|
}
|
|
return list(ausleihungen.find(query))
|
|
except Exception as e:
|
|
print(f"Error in get_ausleihungen_ending_now: {e}")
|
|
return []
|
|
|
|
|
|
def activate_ausleihung(id):
|
|
"""Aktiviert eine geplante Ausleihe."""
|
|
try:
|
|
doc_id = ObjectId(id) if isinstance(id, str) else id
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
ausleihungen = db['ausleihungen']
|
|
|
|
result = ausleihungen.update_one(
|
|
{'_id': doc_id, 'Status': 'planned'},
|
|
{'$set': {
|
|
'Status': 'active',
|
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
}}
|
|
)
|
|
return result.modified_count > 0
|
|
except Exception as e:
|
|
print(f"Error activating ausleihung: {e}")
|
|
return False
|
|
|
|
|
|
def reset_item_completely(item_id):
|
|
"""Setzt den Ausleihstatus eines Items vollständig zurück."""
|
|
try:
|
|
doc_id = ObjectId(item_id) if isinstance(item_id, str) else item_id
|
|
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
items_collection = db['items']
|
|
ausleihungen_collection = db['ausleihungen']
|
|
|
|
item = items_collection.find_one({'_id': doc_id})
|
|
if not item:
|
|
return {'success': False, 'message': 'Item nicht gefunden'}
|
|
|
|
item_name = item.get('Name', 'Unbekannt')
|
|
now_utc = datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
|
|
# 1. Bulk update active borrowings
|
|
update_res = ausleihungen_collection.update_many(
|
|
{
|
|
'Item': str(item_id),
|
|
'Status': {'$in': ['active', 'planned']}
|
|
},
|
|
{
|
|
'$set': {
|
|
'Status': 'completed',
|
|
'End': now_utc,
|
|
'LastUpdated': now_utc,
|
|
'CompletedBy': 'System Reset'
|
|
}
|
|
}
|
|
)
|
|
completed_count = update_res.modified_count
|
|
|
|
# 2. Reset Item document with valid PyMongo update query format
|
|
update_query = {
|
|
'$set': {
|
|
'Verfuegbar': True,
|
|
'LastUpdated': now_utc
|
|
}
|
|
}
|
|
|
|
unsets = {}
|
|
if 'User' in item:
|
|
unsets['User'] = ''
|
|
if 'BorrowerInfo' in item:
|
|
unsets['BorrowerInfo'] = ''
|
|
if 'ExemplareStatus' in item:
|
|
unsets['ExemplareStatus'] = ''
|
|
|
|
if unsets:
|
|
update_query['$unset'] = unsets
|
|
|
|
result = items_collection.update_one({'_id': doc_id}, update_query)
|
|
|
|
return {
|
|
'success': True,
|
|
'message': f'Item "{item_name}" wurde erfolgreich zurückgesetzt',
|
|
'details': {
|
|
'completed_borrowings': completed_count,
|
|
'item_reset': result.modified_count > 0
|
|
}
|
|
}
|
|
except Exception as e:
|
|
print(f"Error resetting item: {e}")
|
|
return {'success': False, 'message': f'Fehler beim Zurücksetzen: {e}'}
|
|
|
|
|
|
# === KOMPATIBILITÄTSFUNKTIONEN ===
|
|
|
|
def add_planned_booking(item_id, user_id, start_date, end_date, notes="", period=None):
|
|
return add_ausleihung(item_id, user_id, start_date, end_date, notes, status='planned', period=period)
|
|
|
|
|
|
def check_booking_conflict(item_id, start_date, end_date, period=None):
|
|
return check_ausleihung_conflict(item_id, start_date, end_date, period)
|
|
|
|
|
|
def mark_booking_active(booking_id, ausleihung_id=None):
|
|
try:
|
|
doc_id = ObjectId(booking_id) if isinstance(booking_id, str) else booking_id
|
|
update_data = {
|
|
'Status': 'active',
|
|
'LastUpdated': datetime.datetime.now(ZoneInfo("Europe/Berlin"))
|
|
}
|
|
if ausleihung_id:
|
|
update_data['AusleihungId'] = ausleihung_id
|
|
|
|
with _get_client() as client:
|
|
db = client[cfg.MONGODB_DB]
|
|
res = db['ausleihungen'].update_one({'_id': doc_id}, {'$set': update_data})
|
|
return res.matched_count > 0
|
|
except Exception as e:
|
|
print(f"Error activating booking: {e}")
|
|
return activate_ausleihung(booking_id)
|
|
|
|
|
|
def get_bookings_starting_now(current_time):
|
|
return get_ausleihungen_starting_now(current_time)
|
|
|
|
|
|
def get_bookings_ending_now(current_time):
|
|
return get_ausleihungen_ending_now(current_time) |