Files
Inventarsystem/Web/modules/database/ausleihung.py
T
Aiirondev_dev 0c27d7ac86 feat: Add Excel, PDF export, and user generation modules
- Implemented `excel_export.py` for generating library item exports in Excel format.
- Created `pdf_export.py` for generating audit reports compliant with DIN 5008 standards, including detailed event tables and signature blocks.
- Developed `generate_user.py` for interactive user creation with validation for usernames and passwords.
- Introduced `module_registry.py` for managing module states and path matching.
- Added a basic `__init__.py` in the `terminplaner` module for initialization.
2026-05-20 15:30:23 +02:00

1239 lines
45 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' (abgeschlossen), 'cancelled' (storniert)
"""
'''
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
import datetime
import pytz
from datetime import timezone
import os
import json
import shutil
import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient
# Add this helper function after imports
def ensure_timezone_aware(dt):
"""Ensures a datetime is timezone-aware, using UTC if naive"""
if dt is None:
return None
if dt.tzinfo is None:
# Treat naive datetimes as UTC
return dt.replace(tzinfo=None)
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()
start_time = ausleihung.get('Start')
end_time = 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'
# Wenn kein Enddatum gesetzt ist oder die aktuelle Zeit vor dem Enddatum liegt,
# ist die Ausleihung aktiv
elif not end_time or current_time < end_time:
new_status = 'active'
# Wenn die aktuelle Zeit nach dem Enddatum liegt, ist die Ausleihung beendet
else:
new_status = 'completed'
# 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.logs.ausleihung_log as ausleihung_log
ausleihung_log.log_status_change(
str(ausleihung['_id']),
original_status,
new_status,
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().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().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, start_date, end_date=None, notes="", status="active", period=None, exemplar_data=None):
"""
Add a new borrowing record for an item.
Args:
item_id (str): ID of the item borrowed
user (str): Username 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': user,
'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
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):
"""
Update an existing ausleihung record.
Args:
id (str): ID of the ausleihung to update
item_id (str, optional): New item ID
user_id (str, optional): New user ID
start (datetime, optional): New start time
end (datetime, optional): New end time
notes (str, optional): New notes
status (str, optional): New status
period (int, optional): New period
Returns:
bool: True if successful, False otherwise
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
# Build update data with only the fields that are provided
update_data = {}
if item_id is not None:
update_data['Item'] = item_id
if user_id is not None:
update_data['User'] = user_id
if start is not None:
# Ensure timezone-aware datetime
update_data['Start'] = ensure_timezone_aware(start)
if end is not None:
# Ensure timezone-aware datetime
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
# Always update the LastUpdated timestamp
update_data['LastUpdated'] = datetime.datetime.now()
# Perform the update
result = ausleihungen.update_one(
{'_id': ObjectId(id)},
{'$set': update_data}
)
client.close()
# Log the update for debugging
print(f"Updated ausleihung {id}: modified_count={result.modified_count}, update_data={update_data}")
return result.modified_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()
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()
}}
)
item.update_one(
{'_id': ObjectId(id)},
{'$set': {
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
}}
)
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)},
{'$set': {
'Status': 'cancelled',
'LastUpdated': datetime.datetime.now()
}}
)
client.close()
return result.modified_count > 0
except Exception as e:
# print(f"Error cancelling ausleihung: {e}") # Log the error
return False
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()
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'):
"""
Ruft Ausleihungen nach verschiedenen Kriterien ab.
Args:
status (str/list, optional): Status(se) der Ausleihungen ('planned', 'active', 'completed', 'cancelled')
start (str/datetime, optional): Startdatum für Datumsfilterung
end (str/datetime, optional): Enddatum für Datumsfilterung
date_filter (str, optional): Art des Datumsfilters ('overlap', 'start_in', 'end_in', 'contained')
Returns:
list: Liste von Ausleihungsdatensätzen
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
collection = db['ausleihungen']
# Query erstellen
query = {'Status': {'$ne': 'deleted'}}
# Status-Filter hinzufügen
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__'
# Datum parsen, wenn als String angegeben
if start is not None and isinstance(start, str):
try:
from dateutil import parser
start = parser.parse(start)
except:
start = None
if end is not None and isinstance(end, str):
try:
from dateutil import parser
end = parser.parse(end)
except:
end = None
# Datumsfilter hinzufügen
if start is not None and end is not None:
if date_filter == 'overlap':
# Überlappende Ausleihungen (Standard)
query['$or'] = [
# Ausleihe beginnt im Bereich
{'Start': {'$gte': start, '$lte': end}},
# Ausleihe endet im Bereich
{'End': {'$gte': start, '$lte': end}},
# Ausleihe umfasst den gesamten Bereich
{'Start': {'$lte': start}, 'End': {'$gte': end}},
# Aktive Ausleihungen ohne Ende, die vor dem Ende beginnen
{'Start': {'$lte': end}, 'End': None}
]
elif date_filter == 'start_in':
# Nur Ausleihungen, die im Bereich beginnen
query['Start'] = {'$gte': start, '$lte': end}
elif date_filter == 'end_in':
# Nur Ausleihungen, die im Bereich enden
query['End'] = {'$gte': start, '$lte': end}
elif date_filter == 'contained':
# Nur Ausleihungen, die vollständig im Bereich liegen
query['Start'] = {'$gte': start}
query['End'] = {'$lte': end}
results = list(collection.find(query))
client.close()
return results
except Exception as e:
# print(f"Error retrieving ausleihungen: {e}") # Log the error
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 und verifiziert den Status clientseitig.
Args:
user_id (str): ID oder Benutzername des Benutzers
status (str/list, optional): Status(se) der Ausleihungen
use_client_side_verification (bool, optional): Ob der Status clientseitig verifiziert werden soll
Returns:
list: Liste von Ausleihungsdatensätzen des Benutzers
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
query = {'User': user_id, 'Status': {'$ne': 'deleted'}}
# Wenn clientseitige Verifikation verwendet wird, holen wir ALLE Ausleihungen
# und filtern später clientseitig
if use_client_side_verification:
# Bei clientseitiger Verifikation alle Ausleihungen holen (auch cancelled)
# da wir den Status später neu berechnen
pass # query bleibt unverändert
else:
# Exclude only cancelled status by default - we want to see planned, active, and completed
if status is not None:
if isinstance(status, list):
query['Status'] = {'$in': status}
else:
query['Status'] = status
else:
# Otherwise exclude only cancelled appointments
query['Status'] = {'$ne': 'cancelled'}
# Get appointments from database
if not use_client_side_verification:
results = list(ausleihungen.find(query))
client.close()
return results
# Wenn clientseitige Statusverifikation aktiviert ist, holen wir alle Ausleihungen
# des Benutzers und verifizieren den Status anschließend
all_ausleihungen = list(ausleihungen.find(query))
client.close()
# Immer clientseitige Statusverifikation durchführen wenn aktiviert
if use_client_side_verification:
for ausleihung in all_ausleihungen:
# Clientseitige Statusverifizierung für alle Ausleihungen
current_status = get_current_status(ausleihung)
ausleihung['VerifiedStatus'] = current_status
# Wenn keine Filterung erforderlich ist, geben wir alle Ausleihungen zurück
if status is None:
return all_ausleihungen
# Statusfilterung durchführen
filtered_results = []
for ausleihung in all_ausleihungen:
# Clientseitige Statusverifizierung
current_status = get_current_status(ausleihung)
# Status-Matching
if isinstance(status, list):
if current_status in status and current_status != 'deleted':
# Status aktualisieren und zur Ergebnismenge hinzufügen
ausleihung['VerifiedStatus'] = current_status
filtered_results.append(ausleihung)
else:
if current_status == status and current_status != 'deleted':
# Status aktualisieren und zur Ergebnismenge hinzufügen
ausleihung['VerifiedStatus'] = current_status
filtered_results.append(ausleihung)
return filtered_results
except Exception as e:
# print(f"Error retrieving ausleihungen for user {user_id}: {e}") # Log the error
return []
def get_ausleihung_by_item(item_id, status=None, include_history=False):
"""
Get ausleihung record(s) for a specific item.
Args:
item_id (str): ID of the item
status (str, optional): Filter by status
include_history (bool): If True, return the most recent record regardless of status
Returns:
dict or None: Ausleihung record or None if not found
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
# Build query
query = {'Item': item_id, 'Status': {'$ne': 'deleted'}}
if status and not include_history:
query['Status'] = status
# Get the most recent record by sorting by Start date descending
ausleihung = ausleihungen.find(query).sort('Start', -1).limit(1)
result = None
for record in ausleihung:
record['_id'] = str(record['_id'])
result = record
break
client.close()
return result
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.
Args:
start_date (datetime): Beginn des Zeitraums
end_date (datetime): Ende des Zeitraums
status (str/list, optional): Status(se) der Ausleihungen
Returns:
list: Liste von Ausleihungsdatensätzen im Zeitraum
"""
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.
Args:
item_id (str): ID des zu prüfenden Gegenstands
start_date (datetime): Vorgeschlagenes Startdatum
end_date (datetime): Vorgeschlagenes Enddatum
period (int, optional): Schulstunde für die Prüfung
Returns:
bool: True, wenn ein Konflikt besteht, sonst False
"""
try:
print(f"Checking booking conflict for item {item_id}, period {period}, start {start_date}, end {end_date}")
if start_date and hasattr(start_date, 'tzinfo') and start_date.tzinfo:
start_date = start_date.replace(tzinfo=None)
if end_date and hasattr(end_date, 'tzinfo') and end_date.tzinfo:
end_date = end_date.replace(tzinfo=None)
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
# Get the date component for filtering
booking_date = start_date.date()
# First, get all active and planned bookings for this item
all_bookings = list(ausleihungen.find({
'Item': item_id,
'Status': {'$in': ['planned', 'active']}
}))
# Print all relevant bookings for debugging
print(f"Found {len(all_bookings)} existing bookings for this item")
for bk in all_bookings:
bk_id = str(bk.get('_id'))
bk_status = bk.get('Status')
bk_period = bk.get('Period', 'None')
bk_start = bk.get('Start')
bk_user = bk.get('User')
print(f" - Booking {bk_id}: Status={bk_status}, Period={bk_period}, Start={bk_start}, User={bk_user}")
# If we're booking by period, check for period conflicts
if period is not None:
period_int = int(period)
# Check bookings on the same day with the same period
for booking in all_bookings:
booking_start = booking.get('Start')
if not booking_start:
continue
# Compare just the date part
try:
# Ensure we're comparing date objects, not datetime objects
existing_date = booking_start.date()
if existing_date == booking_date:
# If this booking has the same period, it's a conflict
booking_period = booking.get('Period')
# Convert to integer for proper comparison
if booking_period is not None and int(booking_period) == period_int:
print(f"CONFLICT: Same day, same period. Period: {period_int}, Date: {booking_date}")
client.close()
return True
except Exception as e:
print(f"Error comparing dates: {e}")
# Continue checking other bookings if there's an error with one
# Always check for time overlaps, regardless of whether period was specified
for booking in all_bookings:
booking_start = booking.get('Start')
booking_end = booking.get('End')
if not booking_start:
continue
# Set default end time if not specified
if not booking_end:
booking_end = booking_start + datetime.timedelta(hours=1)
# Check for overlap
# 1. New booking starts during existing booking
# 2. New booking ends during existing booking
# 3. New booking completely contains existing booking
# 4. Existing booking completely contains new booking
if ((start_date >= booking_start and start_date < booking_end) or
(end_date > booking_start and end_date <= booking_end) or
(start_date <= booking_start and end_date >= booking_end) or
(start_date >= booking_start and end_date <= booking_end)):
print(f"CONFLICT: Time overlap. New booking: {start_date}-{end_date}, Existing: {booking_start}-{booking_end}")
client.close()
return True
print("No conflicts found!")
client.close()
return False
except Exception as e:
print(f"Error checking booking conflicts: {e}")
import traceback
traceback.print_exc()
return True # Bei Fehler Konflikt annehmen, um auf Nummer sicher zu gehen
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
Args:
item_id (str): ID of the item to check
start_date (datetime): Start time for the booking
end_date (datetime): End time for the booking
period (int): Optional period number (for period-based booking)
period_end (int): Optional end period number for period ranges
Returns:
bool: True if there's a conflict, False otherwise
"""
try:
if start_date and hasattr(start_date, 'tzinfo') and start_date.tzinfo:
start_date = start_date.replace(tzinfo=None)
if end_date and hasattr(end_date, 'tzinfo') and end_date.tzinfo:
end_date = end_date.replace(tzinfo=None)
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
# Get the date component for filtering
booking_date = start_date.date()
# First, get all active and planned bookings for this item
all_bookings = list(ausleihungen.find({
'Item': item_id,
'Status': {'$in': ['planned', 'active']}
}))
# Print all relevant bookings for debugging
print(f"Found {len(all_bookings)} existing bookings for this item")
# If we're booking by period, check for period conflicts
if period is not None:
period_start = int(period)
periods_to_check = [period_start]
# If period_end is specified, it's a range of periods
if period_end is not None:
period_end = int(period_end)
periods_to_check = list(range(period_start, period_end + 1))
# Check bookings on the same day with any overlapping period
for booking in all_bookings:
booking_start = booking.get('Start')
if not booking_start:
continue
# Compare just the date part
existing_date = booking_start.date()
if existing_date == booking_date:
booking_period = booking.get('Period')
# Normalize to int if possible
try:
booking_period_int = int(booking_period) if booking_period is not None else None
except Exception:
booking_period_int = None
# If this booking has any period in our range, it's a conflict
if booking_period_int is not None and booking_period_int in periods_to_check:
print(f"CONFLICT: Same day, overlapping period. Booking period: {booking_period_int}")
client.close()
return True
# Always also check time overlaps against any existing bookings (incl. those without Period)
for booking in all_bookings:
booking_start = booking.get('Start')
booking_end = booking.get('End')
if not booking_start:
continue
# Set default end time if not specified
if not booking_end:
booking_end = booking_start + datetime.timedelta(hours=1)
# Check for overlap of [start_date, end_date] with [booking_start, booking_end]
if ((start_date >= booking_start and start_date < booking_end) or
(end_date > booking_start and end_date <= booking_end) or
(start_date <= booking_start and end_date >= booking_end) or
(start_date >= booking_start and end_date <= booking_end)):
print(f"CONFLICT: Time overlap. New: {start_date}-{end_date}, Existing: {booking_start}-{booking_end}")
client.close()
return True
print("No conflicts found!")
client.close()
return False
except Exception as e:
print(f"Error checking booking conflicts: {e}")
import traceback
traceback.print_exc()
return True # Assume conflict on error for safety
# === AUTOMATISIERTE VERARBEITUNG ===
def get_ausleihungen_starting_now(current_time):
"""
Ruft Ausleihungen ab, die jetzt beginnen sollen (innerhalb eines Zeitfensters).
Args:
current_time (datetime): Aktuelle Zeit für den Vergleich
Returns:
list: Liste von Ausleihungen, die jetzt beginnen sollen
"""
try:
# Define a wider time window (3 hours before to 1 hour after)
# This helps catch bookings that might have been missed
hours_before = datetime.timedelta(hours=3)
hours_after = datetime.timedelta(hours=1)
start_time = current_time - hours_before
end_time = current_time + hours_after
# Get today's date for date comparison
today = current_time.date()
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
# Build a query to find planned bookings that:
# 1. Are scheduled to start within our time window
# 2. OR have a period set for today
query = {
'Status': 'planned',
'$or': [
# Time-based bookings within our window
{'Start': {'$lte': end_time, '$gte': start_time}},
# Period-based bookings for today
{
'Period': {'$exists': True},
'Start': {
'$gte': datetime.datetime.combine(today, datetime.time.min),
'$lt': datetime.datetime.combine(today + datetime.timedelta(days=1), datetime.time.min)
}
}
]
}
print(f"Query for bookings starting now: {query}")
bookings = list(ausleihungen.find(query))
print(f"Found {len(bookings)} bookings that might be starting now")
for b in bookings:
print(f" - Booking {b.get('_id')}: Start={b.get('Start')}, Period={b.get('Period')}")
client.close()
return bookings
except Exception as e:
print(f"Error in get_ausleihungen_starting_now: {e}")
import traceback
traceback.print_exc()
return []
def get_ausleihungen_ending_now(current_time):
"""
Ruft Ausleihungen ab, die jetzt enden sollen (innerhalb eines Zeitfensters).
Args:
current_time (datetime): Aktuelle Zeit für den Vergleich
Returns:
list: Liste von Ausleihungen, die jetzt enden sollen
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
# Create a wider time window (15 minutes before to catch any missed endings)
window_before = datetime.timedelta(minutes=15)
window_after = datetime.timedelta(minutes=5)
start_time = current_time - window_before
end_time = current_time + window_after
# Get today's date for period-based checks
today = current_time.date()
# Find active bookings that:
# 1. Have an end time within our window, OR
# 2. Are from today with a period (will check the period in process_bookings)
query = {
'Status': 'active',
'$or': [
{'End': {'$gte': start_time, '$lte': end_time}},
{
'Period': {'$exists': True},
'Start': {
'$gte': datetime.datetime.combine(today, datetime.time.min),
'$lt': datetime.datetime.combine(today + datetime.timedelta(days=1), datetime.time.min)
}
}
]
}
print(f"Looking for bookings ending now with query: {query}")
bookings = list(ausleihungen.find(query))
print(f"Found {len(bookings)} bookings that might be ending now")
for b in bookings:
print(f" - Potential ending booking {b.get('_id')}: End={b.get('End')}, Period={b.get('Period')}")
client.close()
return bookings
except Exception as e:
print(f"Error in get_ausleihungen_ending_now: {e}")
import traceback
traceback.print_exc()
return []
def activate_ausleihung(id):
"""
Aktiviert eine geplante Ausleihe.
Args:
id (str): ID der zu aktivierenden 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']
# Zuerst prüfen, ob die Ausleihe existiert und den Status 'planned' hat
ausleihung = ausleihungen.find_one({'_id': ObjectId(id)})
if not ausleihung or ausleihung.get('Status') != 'planned':
client.close()
return False
# Ausleihe aktivieren
result = ausleihungen.update_one(
{'_id': ObjectId(id)},
{'$set': {
'Status': 'active',
'LastUpdated': datetime.datetime.now()
}}
)
client.close()
return result.modified_count > 0
except Exception as e:
return False
# === KOMPATIBILITÄTSFUNKTIONEN ===
# Hilfsmethoden für alte Funktionsaufrufe, um Abwärtskompatibilität zu gewährleisten
def add_planned_booking(item_id, user, start_date, end_date, notes="", period=None):
"""Kompatibilitätsfunktion - erstellt eine geplante Ausleihe"""
return add_ausleihung(item_id, user, start_date, end_date, notes, status='planned', period=period)
def check_booking_conflict(item_id, start_date, end_date, period=None):
"""Kompatibilitätsfunktion - prüft auf Ausleihungskonflikte mit Periodenunterstützung"""
return check_ausleihung_conflict(item_id, start_date, end_date, period)
def cancel_booking(booking_id):
"""Kompatibilitätsfunktion - storniert eine Ausleihe"""
return cancel_ausleihung(booking_id)
def get_booking(booking_id):
"""Kompatibilitätsfunktion - ruft eine einzelne Ausleihe ab"""
return get_ausleihung(booking_id)
def get_active_bookings(start=None, end=None):
"""Kompatibilitätsfunktion - ruft aktive Ausleihungen ab"""
return get_active_ausleihungen(start, end)
def get_planned_bookings(start=None, end=None):
"""Kompatibilitätsfunktion - ruft geplante Ausleihungen ab"""
return get_planned_ausleihungen(start, end)
def get_completed_bookings(start=None, end=None):
"""Kompatibilitätsfunktion - ruft abgeschlossene Ausleihungen ab"""
return get_completed_ausleihungen(start, end)
def mark_booking_active(booking_id, ausleihung_id=None):
"""Kompatibilitätsfunktion - markiert eine Ausleihe als aktiv und verknüpft optional eine Ausleihungs-ID"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
ausleihungen = db['ausleihungen']
# Basisupdate-Daten mit Status-Änderung
update_data = {
'Status': 'active',
'LastUpdated': datetime.datetime.now()
}
# Wenn eine Ausleihungs-ID angegeben wurde, diese auch verknüpfen
if ausleihung_id:
update_data['AusleihungId'] = ausleihung_id
# Update durchführen
result = ausleihungen.update_one(
{'_id': ObjectId(booking_id)},
{'$set': update_data}
)
client.close()
return result.modified_count > 0
except Exception as e:
print(f"Error activating booking: {e}")
# Fallback zur alten Methode bei Fehlern
return activate_ausleihung(booking_id)
def mark_booking_completed(booking_id):
"""Kompatibilitätsfunktion - markiert eine Ausleihe als abgeschlossen"""
return complete_ausleihung(booking_id)
def get_bookings_starting_now(current_time):
"""Kompatibilitätsfunktion - ruft startende Ausleihungen ab"""
def get_bookings_starting_now(current_time):
"""Kompatibilitätsfunktion - ruft startende Ausleihungen ab"""
return get_ausleihungen_starting_now(current_time)
def get_bookings_ending_now(current_time):
"""Kompatibilitätsfunktion - ruft endende Ausleihungen ab"""
return get_ausleihungen_ending_now(current_time)
def reset_item_completely(item_id):
"""
Setzt den Ausleihstatus eines Items vollständig zurück.
Diese Funktion:
- Markiert das Item als verfügbar
- Löscht alle Ausleihungsinformationen
- Setzt Exemplar-Status zurück
- Beendet alle aktiven Ausleihungen
Args:
item_id (str): Die ID des Items das zurückgesetzt werden soll
Returns:
dict: Erfolg-/Fehlerstatus mit Details
"""
try:
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db = client[cfg.MONGODB_DB]
items_collection = db['items']
ausleihungen_collection = db['ausleihungen']
# Item abrufen
item = items_collection.find_one({'_id': ObjectId(item_id)})
if not item:
return {'success': False, 'message': 'Item nicht gefunden'}
item_name = item.get('Name', 'Unbekannt')
# 1. Alle aktiven Ausleihungen für dieses Item beenden
active_borrowings = ausleihungen_collection.find({
'Item': item_id,
'Status': {'$in': ['active', 'planned']}
})
completed_count = 0
for borrowing in active_borrowings:
ausleihungen_collection.update_one(
{'_id': borrowing['_id']},
{
'$set': {
'Status': 'completed',
'End': datetime.datetime.now(),
'LastUpdated': datetime.datetime.now(),
'CompletedBy': 'System Reset'
}
}
)
completed_count += 1
# 2. Item-Status zurücksetzen
update_data = {
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
}
# Entferne User-Zuordnung falls vorhanden
if 'User' in item:
update_data['$unset'] = {'User': ''}
# Entferne BorrowerInfo falls vorhanden
if 'BorrowerInfo' in item:
if '$unset' not in update_data:
update_data['$unset'] = {}
update_data['$unset']['BorrowerInfo'] = ''
# Setze ExemplareStatus zurück falls vorhanden
if 'ExemplareStatus' in item:
if '$unset' not in update_data:
update_data['$unset'] = {}
update_data['$unset']['ExemplareStatus'] = ''
# Item aktualisieren
result = items_collection.update_one(
{'_id': ObjectId(item_id)},
update_data
)
client.close()
if result.modified_count > 0:
return {
'success': True,
'message': f'Item "{item_name}" wurde erfolgreich zurückgesetzt',
'details': {
'completed_borrowings': completed_count,
'item_reset': True
}
}
else:
return {
'success': True,
'message': f'Item "{item_name}" war bereits im korrekten Status',
'details': {
'completed_borrowings': completed_count,
'item_reset': False
}
}
except Exception as e:
return {
'success': False,
'message': f'Fehler beim Zurücksetzen: {str(e)}'
}