Compare commits

...

15 Commits

Author SHA1 Message Date
Aiirondev_dev 6f9994c4ce Addtion of missing import
Release Inventarsystem / release-docker (push) Successful in 3m9s
2026-08-23 10:27:36 +02:00
Aiirondev_dev 6496716572 Slight changes to the Design
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 16:13:45 +02:00
Aiirondev_dev f750739b51 Testing of the mahnugnssystem
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 16:02:39 +02:00
Aiirondev_dev 4c3545f4ba Design changes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-22 15:44:29 +02:00
Aiirondev_dev c0bf0c0b8e Mahnungswesen implementierung
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 15:38:36 +02:00
Aiirondev_dev 2cd95ef808 Removal again
Release Inventarsystem / release-docker (push) Successful in 3m6s
2026-08-22 15:14:26 +02:00
Aiirondev_dev 17b58ff7ca Removal again
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-22 00:08:38 +02:00
Aiirondev_dev c67f64a45a Try again
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-22 00:02:47 +02:00
Aiirondev_dev 7727bfd94a Revert of changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-21 23:44:26 +02:00
Aiirondev_dev 55f34e0462 changes to the info importing for the library item
Release Inventarsystem / release-docker (push) Successful in 2m27s
2026-08-21 23:28:52 +02:00
Aiirondev_dev 5a3a180b88 Removal of the Korrektion function to remove an unneedet administrative ioverhead, but an option to add it back in later
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-21 23:04:37 +02:00
Aiirondev_dev 07ad592bd9 Improvement of naming
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-21 22:22:37 +02:00
Aiirondev_dev fa299cde06 Redirect issue with the Bibiliothek öffnen Button in the library_item_invoices.html
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-21 21:53:08 +02:00
Aiirondev_dev bb2c3f52e8 Implementation for the Damage history for the library items
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-21 18:10:31 +02:00
Aiirondev_dev 58ea4e5716 Implementation for the Damage history for the library items
Release Inventarsystem / release-docker (push) Successful in 2m22s
2026-08-21 17:45:55 +02:00
6 changed files with 720 additions and 106 deletions
+473 -85
View File
@@ -49,6 +49,7 @@ import Web.modules.log.audit_log as al
import push_notifications as pn
import Web.modules.inventarsystem.pdf_export as pdf_export
import Web.modules.inventarsystem.excel_export as excel_export
from Web.modules.emailservice.email import send
import datetime
from apscheduler.schedulers.background import BackgroundScheduler
from bson.objectid import ObjectId, InvalidId
@@ -1588,8 +1589,7 @@ def update_appointment_statuses():
f"Failed to create activation notification for {appointment.get('_id')}: {notif_err}"
)
elif it.is_library_item(appointment.get('Item')):
# Introduction of an messaging system and a Mahnstufen implementation for the library Book bookings after the designatet time.
pass
create_return_reminders()
client.close()
@@ -1713,6 +1713,111 @@ def _shutdown_scheduler():
atexit.register(_shutdown_scheduler)
def create_return_reminders():
"""
Prüft aktive Ausleihungen auf Überschreitung der Rückgabefrist,
erhöht Mahnstufen, versendet E-Mails/Benachrichtigungen und sperrt Nutzer.
"""
current_time = datetime.datetime.now(datetime.timezone.utc)
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
ausleihungen = db['ausleihungen']
users_col = db['users']
items_col = db['items']
# Finde alle aktiven Ausleihungen, die in der Vergangenheit fällig waren
overdue_appointments = list(ausleihungen.find({
'Status': 'active',
'DueDate': {'$lt': current_time}
}))
for appt in overdue_appointments:
days_overdue = (current_time - appt.get('DueDate', current_time)).days
mahnstufe = appt.get('Mahnstufe', 0)
target_user = str(appt.get('User', '')).strip()
item_id = appt.get('Item')
# 1. Objektdetails holen (für den E-Mail-Text)
item_name = "Unbekannter Artikel"
if item_id:
try:
item_doc = items_col.find_one({'_id': ObjectId(item_id)})
if item_doc:
item_name = item_doc.get('Name', str(item_id))
except Exception:
pass
# 2. Nutzerdetails holen (für die E-Mail-Adresse)
user_doc = users_col.find_one({'username': target_user}) # Feldnamen ggf. anpassen (z.B. '_id')
if not user_doc:
app.logger.warning(f"Nutzer '{target_user}' für Mahnung nicht gefunden.")
continue
user_email = user_doc.get('email')
# 3. Eskalationsstufen prüfen
if days_overdue >= 14 and days_overdue < 28 and mahnstufe == 0:
# STUFE 1: Erste Mahnung
ausleihungen.update_one(
{'_id': appt['_id']},
{'$set': {'Mahnstufe': 1, 'LastUpdated': current_time}}
)
_create_notification(
db, audience='user', notif_type='warning',
title='Erinnerung: Ausleihe überfällig',
message=f'Deine Ausleihe für "{item_name}" ist seit {days_overdue} Tagen überfällig.',
target_user=target_user, severity='warning'
)
if user_email:
subject = "1. Mahnung: Rückgabe überfällig"
note = (f"Hallo {target_user},<br><br>"
f"bitte beachte, dass die Ausleihe für den Artikel <b>{item_name}</b> "
f"seit {days_overdue} Tagen überfällig ist. Bitte bringe den Artikel zeitnah zurück.")
send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
app.logger.info(f"1. Mahnung an {user_email} gesendet.")
elif days_overdue >= 28 and mahnstufe == 1:
# STUFE 2: Letzte Mahnung, Sperrung und Report
ausleihungen.update_one(
{'_id': appt['_id']},
{'$set': {'Mahnstufe': 2, 'LastUpdated': current_time}}
)
# Nutzerkonto für weitere Ausleihen sperren
users_col.update_one(
{'_id': user_doc['_id']},
{'$set': {
'is_blocked': True,
'block_reason': f'System-Sperre: Ausleihe von "{item_name}" {days_overdue} Tage überfällig.'
}}
)
_create_notification(
db, audience='user', notif_type='error',
title='Konto gesperrt - 2. Mahnung',
message=f'Aufgrund der starken Überfälligkeit von "{item_name}" wurde dein Konto vorübergehend gesperrt.',
target_user=target_user, severity='critical'
)
if user_email:
subject = "WICHTIG: Kontosperrung & 2. Mahnung"
note = (f"Hallo {target_user},<br><br>"
f"deine Ausleihe für den Artikel <b>{item_name}</b> ist nun seit {days_overdue} Tagen überfällig. "
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.")
send(email=user_email, subject=subject, note=note, sender="Bibliotheksverwaltung")
app.logger.warning(f"Nutzer {target_user} gesperrt. 2. Mahnung an {user_email} gesendet.")
client.close()
except Exception as e:
app.logger.error(f"Fehler bei der automatischen Mahnlauf-Prüfung: {e}")
"""-------------------------------------------------------------File Upload Validation----------------------------------------------------------------------------- """
def allowed_file(filename, file_content=None, max_size_mb=cfg.MAX_UPLOAD_MB):
@@ -3312,13 +3417,17 @@ def library_view():
def library_loans_admin():
"""Admin overview for library borrowings and damaged library items."""
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')
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'])
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')
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'):
@@ -3337,7 +3446,6 @@ def library_loans_admin():
return _format_money_value(value)
def safe_decrypt(val):
"""Helper to decrypt strings that start with enc:: or standard encrypted strings."""
if not val or not isinstance(val, str):
return val or ''
try:
@@ -3345,19 +3453,48 @@ def library_loans_admin():
except Exception:
return val
# Hilfsfunktion zur Formatierung der Schadenshistorie
def format_damage_history(reports, repairs):
fmt_reports = []
for r in (reports or []):
fmt_reports.append({
'description': r.get('description', ''),
'reported_by': r.get('reported_by', ''),
'reported_at': fmt_dt(r.get('reported_at'))
})
fmt_repairs = []
for rep in (repairs or []):
res_reports = []
for res in rep.get('resolved_reports', []):
res_reports.append({
'description': res.get('description', ''),
'reported_by': res.get('reported_by', ''),
'reported_at': fmt_dt(res.get('reported_at'))
})
fmt_repairs.append({
'repaired_by': rep.get('repaired_by', ''),
'repaired_at': fmt_dt(rep.get('repaired_at')),
'resolved_reports': res_reports
})
return fmt_reports, fmt_repairs
client = None
try:
client = MongoClient(MONGODB_HOST, MONGODB_PORT)
db = client[MONGODB_DB]
items_col = db['items']
ausleihungen_col = db['ausleihungen']
users_col = db['users']
student_cards_col = db['student_cards']
# WICHTIG: 'DamageRepairs': 1 zur Projektion hinzugefügt!
library_items = list(items_col.find(
{'ItemType': {'$in': LIBRARY_ITEM_TYPES}, 'Deleted': {'$ne': True}},
{'Name': 1, 'Code_4': 1, 'Anschaffungskosten': 1, 'Condition': 1, 'HasDamage': 1, 'DamageReports': 1,
'Verfuegbar': 1, 'User': 1, 'ItemType': 1, 'Author': 1, 'ISBN': 1}
{
'Name': 1, 'Code_4': 1, 'Anschaffungskosten': 1, 'Condition': 1,
'HasDamage': 1, 'DamageReports': 1, 'DamageRepairs': 1,
'Verfuegbar': 1, 'User': 1, 'ItemType': 1, 'Author': 1, 'ISBN': 1
}
))
item_map = {str(item['_id']): item for item in library_items if item.get('_id')}
item_ids = list(item_map.keys())
@@ -3370,42 +3507,29 @@ def library_loans_admin():
'Klasse': 1, 'klasse': 1, 'Class': 1, 'school_class': 1}
).sort('Start', -1))
# class_map aufbauen (wie zuvor besprochen)
class_map = {}
student_cards = list(student_cards_col.find({}, {'SchülerName': 1, 'Klasse': 1, 'AusweisId': 1}))
for sc in student_cards:
dec_cls = safe_decrypt(sc.get('Klasse'))
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
dec_name = safe_decrypt(sc.get('SchülerName'))
raw_name = sc.get('SchülerName')
ausweis_id = sc.get('AusweisId')
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
class_map[dec_name] = dec_cls
if raw_name:
class_map[raw_name] = dec_cls
if ausweis_id:
class_map[str(ausweis_id).strip().lower()] = dec_cls
users = list(users_col.find({}, {'Username': 1, 'Name': 1, 'User': 1, 'Klasse': 1, 'klasse': 1, 'Class': 1,
'school_class': 1}))
for u in users:
raw_cls = u.get('Klasse') or u.get('klasse') or u.get('Class') or u.get('school_class') or ''
dec_cls = safe_decrypt(raw_cls)
if not dec_cls:
continue
for key in [u.get('Username'), u.get('Name'), u.get('User')]:
if key:
dec_key = safe_decrypt(key)
class_map[key] = dec_cls
class_map[dec_key] = dec_cls
class_map[dec_key.strip().lower()] = dec_cls
active_item_ids = set()
loan_entries = []
for record in active_records:
item_id = str(record.get('Item') or '')
if item_id and record.get('Status') == 'active':
@@ -3417,26 +3541,24 @@ def library_loans_admin():
invoice_data = record.get('InvoiceData') or {}
condition_value = str(item_doc.get('Condition', '')).strip().lower()
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(
item_doc.get('DamageReports'))
damage_reports = item_doc.get('DamageReports', []) or []
raw_reports = item_doc.get('DamageReports', [])
raw_repairs = item_doc.get('DamageRepairs', [])
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(raw_reports)
fmt_reports, fmt_repairs = format_damage_history(raw_reports, raw_repairs)
raw_user = record.get('User', '')
decrypted_user = safe_decrypt(raw_user)
direct_class = (
user_class = (
safe_decrypt(record.get('Klasse'))
or safe_decrypt(record.get('klasse'))
or safe_decrypt(record.get('Class'))
or safe_decrypt(record.get('school_class'))
)
user_class = (
direct_class
or class_map.get(decrypted_user)
or class_map.get(decrypted_user.strip().lower())
or class_map.get(raw_user)
or ''
or class_map.get(raw_user, '')
or class_map.get(decrypted_user, '')
or class_map.get(decrypted_user.strip().lower(), '')
or ''
)
loan_entries.append({
@@ -3447,7 +3569,7 @@ def library_loans_admin():
'item_author': item_doc.get('Author', ''),
'item_isbn': item_doc.get('ISBN', ''),
'item_cost_raw': item_doc.get('Anschaffungskosten', ''),
'user': decrypted_user,
'user': decrypted_user if decrypted_user else raw_user,
'klasse': user_class,
'status': record.get('Status', ''),
'start': fmt_dt(record.get('Start')),
@@ -3455,24 +3577,32 @@ def library_loans_admin():
'period': record.get('Period', ''),
'notes': record.get('Notes', ''),
'invoice_number': invoice_data.get('invoice_number', ''),
'invoice_amount': fmt_money(invoice_data.get('amount')) if invoice_data.get('amount') is not None else fmt_money(item_doc.get('Anschaffungskosten')),
'invoice_amount': fmt_money(invoice_data.get('amount')) if invoice_data.get(
'amount') is not None else fmt_money(item_doc.get('Anschaffungskosten')),
'invoice_paid': bool(invoice_data.get('paid', False)),
'invoice_paid_at': fmt_dt(invoice_data.get('paid_at')) if isinstance(invoice_data.get('paid_at'), datetime.datetime) else '',
'invoice_corrections_count': len(record.get('InvoiceCorrections', []) or []),
'invoice_paid_at': fmt_dt(invoice_data.get('paid_at')) if isinstance(invoice_data.get('paid_at'),
datetime.datetime) else '',
'has_damage': item_has_damage,
'damage_count': len(damage_reports),
'damage_text': (damage_reports[0].get('description', '') if damage_reports else ''),
'damage_count': len(raw_reports),
'damage_text': fmt_reports[0]['description'] if fmt_reports else '',
'damage_reports': fmt_reports,
'damage_repairs': fmt_repairs
})
damaged_items = []
for item_doc in library_items:
item_id = str(item_doc.get('_id') or '')
condition_value = str(item_doc.get('Condition', '')).strip().lower()
damage_reports = item_doc.get('DamageReports', []) or []
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(damage_reports)
raw_reports = item_doc.get('DamageReports', [])
raw_repairs = item_doc.get('DamageRepairs', [])
item_has_damage = bool(item_doc.get('HasDamage')) or condition_value == 'destroyed' or bool(raw_reports)
if not item_has_damage or item_id in active_item_ids:
continue
fmt_reports, fmt_repairs = format_damage_history(raw_reports, raw_repairs)
damaged_items.append({
'id': item_id,
'name': item_doc.get('Name', ''),
@@ -3480,11 +3610,13 @@ def library_loans_admin():
'author': item_doc.get('Author', ''),
'isbn': item_doc.get('ISBN', ''),
'condition': item_doc.get('Condition', ''),
'damage_count': len(damage_reports),
'damage_text': (damage_reports[0].get('description', '') if damage_reports else ''),
'available': bool(item_doc.get('Verfuegbar', False)),
'last_updated': fmt_dt(item_doc.get('LastUpdated')),
'acquisition_costs': item_doc.get('Anschaffungskosten', "")
'acquisition_costs': item_doc.get('Anschaffungskosten', ""),
'damage_count': len(raw_reports),
'damage_text': fmt_reports[0]['description'] if fmt_reports else '',
'damage_reports': fmt_reports,
'damage_repairs': fmt_repairs
})
return render_template(
@@ -3502,6 +3634,192 @@ def library_loans_admin():
if client:
client.close()
@app.route('/test_mahnungen')
def test_mahnungen():
# Testdaten (Mock-Daten), um alle if/else Bedingungen im HTML zu testen
test_overdue_list = [
{
"user": "Max Mustermann",
"klasse": "10A",
"item_name": "Biologie heute 2",
"due_date": "10.08.2026",
"days_overdue": 12,
"mahnstufe": 2,
"is_blocked": True
},
{
"user": "Anna Schmidt",
"klasse": "9B",
"item_name": "Taschenrechner TI-30",
"due_date": "15.08.2026",
"days_overdue": 7,
"mahnstufe": 1,
"is_blocked": False
},
{
"user": "Lukas Weber",
"klasse": "12",
"item_name": "Faust - Der Tragödie erster Teil",
"due_date": "21.08.2026",
"days_overdue": 1,
"mahnstufe": 0,
"is_blocked": False
}
]
# Render das Template und übergebe die Testdaten
return render_template('mahnungen_admin.html', overdue_list=test_overdue_list, APP_VERSION="1.0.0")
@app.route('/mahnungen_admin')
def mahnungen_admin():
"""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(datetime.timezone.utc)
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()
@app.route('/api/library_items')
def api_library_items():
"""
@@ -9214,7 +9532,7 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
"""
Verarbeitet Items aus der Reparatur und aktualisiert die Serien-Counts
sowie Parent/Child-Abhängigkeiten beim Löschen korrekt.
Entfernt Schadens-Flags komplett aus Items und zugehörigen Ausleihen.
Verschiebt gemeldete Schäden in die Historie (DamageRepairs) bei Reparatur/Austausch.
"""
if not ObjectId.is_valid(item_id):
return False, "Ungültige Item-ID."
@@ -9231,19 +9549,24 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
return False, "Item nicht in der Datenbank gefunden."
series_group_id = item.get('SeriesGroupId')
now = datetime.datetime.now()
damage_unset = {
'HasDamage': "",
legacy_damage_unset = {
'has_damage': "",
'DamageCount': "",
'damage_count': "",
'DamageText': "",
'damage_text': "",
'is_damaged': "",
'IsDamaged': "",
'IsDamaged': ""
}
borrowings_unset = legacy_damage_unset.copy()
borrowings_unset.update({
'HasDamage': "",
'Condition': "",
'DamageReports': ""
}
})
# --- OPTION 1: DELETE ---
if action == 'delete':
@@ -9285,24 +9608,36 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
return True, "Item wurde gelöscht und der Bestand aller Serien-Items angepasst."
current_reports = item.get('DamageReports', [])
repair_entry = {
'repaired_by': current_user,
'repaired_at': now,
'action_taken': action,
'resolved_reports': current_reports
}
# --- OPTION 2: REPLACE ---
elif action == 'replace':
if action == 'replace':
items.update_one(
{'_id': ObjectId(item_id)},
{
'$push': {'DamageRepairs': repair_entry},
'$set': {
'Code_4': str(new_code_4).strip(),
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
'HasDamage': False,
'Condition': 'gut',
'DamageReports': [],
'LastUpdated': now
},
'$unset': damage_unset
'$unset': legacy_damage_unset
}
)
if borrowings is not None:
borrowings.update_many(
{'$or': [{'item_id': str(item_id)}, {'item_id': ObjectId(item_id)}]},
{'$unset': damage_unset}
{'$unset': borrowings_unset}
)
return True, f"Item wurde ersetzt. Neuer Code 4 zugewiesen: {new_code_4}."
@@ -9312,18 +9647,22 @@ def resolve_repaired_item_funct(item_id, action, new_code_4="", current_user="ad
items.update_one(
{'_id': ObjectId(item_id)},
{
'$push': {'DamageRepairs': repair_entry},
'$set': {
'Verfuegbar': True,
'LastUpdated': datetime.datetime.now()
'HasDamage': False,
'Condition': 'gut',
'DamageReports': [],
'LastUpdated': now
},
'$unset': damage_unset
'$unset': legacy_damage_unset
}
)
if borrowings is not None:
borrowings.update_many(
{'$or': [{'item_id': str(item_id)}, {'item_id': ObjectId(item_id)}]},
{'$unset': damage_unset}
{'$unset': borrowings_unset}
)
return True, "Item wurde repariert und ist wieder regulär verfügbar."
@@ -9375,7 +9714,7 @@ def library_item_invoices(item_id):
item_id_str = str(item_id_obj)
# ==========================================
# 1. RECHNUNGSHISTORIE (Bestehender Code)
# 1. RECHNUNGSHISTORIE (Aus Ausleihen)
# ==========================================
borrow_docs = list(ausleihungen.find(
{
@@ -9407,7 +9746,9 @@ def library_item_invoices(item_id):
entries.append({
'borrow_id': str(borrow_doc.get('_id')),
'borrow_status': borrow_doc.get('Status', ''),
'borrow_user': decrypt_text(borrow_doc.get('User', '')),
'borrow_user': decrypt_text(borrow_doc.get('User', '')) if hasattr(borrow_doc,
'get') and borrow_doc.get(
'User') else '',
'borrow_start': borrow_doc.get('Start').strftime('%d.%m.%Y %H:%M') if isinstance(
borrow_doc.get('Start'), datetime.datetime) else '',
'borrow_end': borrow_doc.get('End').strftime('%d.%m.%Y %H:%M') if isinstance(borrow_doc.get('End'),
@@ -9424,36 +9765,83 @@ def library_item_invoices(item_id):
})
# ==========================================
# 2. SCHADENSHISTORIE (Neuer Code)
# 2. SCHADENSHISTORIE (Neues Format + Fallback)
# ==========================================
raw_damages = []
# A: Schäden, die manuell am Element (Item-Dokument) hinterlegt wurden
item_damages = item_doc.get('Damages', [])
if not isinstance(item_damages, list): item_damages = []
# Fallback falls sie klein geschrieben wurden
if isinstance(item_doc.get('damages', []), list):
item_damages.extend(item_doc.get('damages', []))
# A: Aktive / gemeldete Schäden aus 'DamageReports'
damage_reports = item_doc.get('DamageReports', [])
if not isinstance(damage_reports, list):
damage_reports = []
for d in item_damages:
for d in damage_reports:
if not isinstance(d, dict): continue
# Rechnungs-Metadaten anhängen, falls vorhanden
extra_info = ""
if d.get('invoice_number'):
amount = d.get('invoice_amount', '')
extra_info = f"\n(Mit Rechnung: {d['invoice_number']} - {amount}€)"
raw_damages.append({
'raw_date': d.get('date') or d.get('created_at'),
'source': 'Manuell / Element',
'user': d.get('reported_by') or d.get('user') or 'System/Admin',
'description': d.get('description') or d.get('reason') or 'Keine Beschreibung',
'status': d.get('status', 'Unbekannt'),
'raw_date': d.get('reported_at'),
'source': str(d.get('source', 'Meldung')).capitalize(),
'user': d.get('reported_by', 'System/Admin'),
'description': d.get('description', 'Keine Beschreibung') + extra_info,
'status': 'Offen',
'borrow_id': None
})
# B: Schäden, die in Ausleih-Dokumenten vermerkt sind
# B: Behobene Schäden aus 'DamageRepairs'
damage_repairs = item_doc.get('DamageRepairs', [])
if not isinstance(damage_repairs, list):
damage_repairs = []
for r in damage_repairs:
if not isinstance(r, dict): continue
resolved_list = r.get('resolved_reports', [])
repaired_at = r.get('repaired_at')
repaired_by = r.get('repaired_by', 'Admin')
action_taken = r.get('action_taken', 'repair')
# Status anhand der Aktion bestimmen
status_text = 'Repariert' if action_taken == 'repair' else 'Erledigt'
if resolved_list and isinstance(resolved_list, list):
for res in resolved_list:
# Rechnungs-Metadaten aus dem ehemals offenen Report
extra_info = ""
if res.get('invoice_number'):
amount = res.get('invoice_amount', '')
extra_info = f"\n(Mit Rechnung: {res['invoice_number']} - {amount}€)"
raw_damages.append({
'raw_date': res.get('reported_at') or repaired_at,
'source': f"{status_text} von {repaired_by}",
'user': res.get('reported_by', 'System/Admin'),
'description': res.get('description', 'Keine Beschreibung') + extra_info,
'status': status_text,
'borrow_id': None
})
else:
# Fallback, falls keine genauen Reports hinterlegt sind
raw_damages.append({
'raw_date': repaired_at,
'source': 'Reparatur',
'user': repaired_by,
'description': 'Schaden behoben',
'status': status_text,
'borrow_id': None
})
# C: Legacy-Fallback: Schäden, die evtl. nur in alten Ausleih-Dokumenten vermerkt sind
damage_borrow_docs = list(ausleihungen.find(
{
'Item': {'$in': [item_id_str, item_id_obj]},
'$or': [
{'Damage': {'$exists': True, '$ne': ''}},
{'DamageData': {'$exists': True, '$ne': {}}},
{'Condition': {'$exists': True, '$ne': ''}} # Falls Zustand gemeldet wurde
{'Condition': {'$exists': True, '$ne': ''}}
]
}
))
@@ -9469,7 +9857,7 @@ def library_item_invoices(item_id):
if desc and str(desc).strip():
raw_damages.append({
'raw_date': d_data.get('reported_at') or b.get('End') or b.get('Start'),
'source': f"Ausleihe",
'source': "Ausleihe",
'user': borrow_user,
'description': desc,
'status': d_data.get('status', 'Gemeldet'),
@@ -9501,7 +9889,7 @@ def library_item_invoices(item_id):
'isbn': item_doc.get('ISBN', ''),
},
invoices=entries,
damages=damage_entries, # << NEU
damages=damage_entries,
library_module_enabled=cfg.MODULES.is_enabled('library'),
student_cards_module_enabled=cfg.MODULES.is_enabled('student_cards'),
mail_module_enabled=cfg.MODULES.is_enabled('mail')
+1
View File
@@ -1373,6 +1373,7 @@
<li><h6 class="dropdown-header">Bibliotheks-Verwaltung</h6></li>
{% if current_permissions.pages.get('library_loans_admin', False) %}
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Alle Ausleihen/Alle Defekten Items</a></li>
<li><a class="dropdown-item" href="{{ url_for('mahnungen_admin') }}">Mahnungen</a></li>
{% endif %}
{% if student_cards_module_enabled %}
{% if current_permissions.actions.get('can_manage_users', False) %}
+5 -5
View File
@@ -299,7 +299,7 @@
<div class="muted">{{ e.item_author or '—' }}</div>
<div class="mono">{{ e.item_code or '—' }}</div>
<div style="margin-top:6px;">
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('library_item_invoices', item_id=e.item_id) }}">Rechnungen</a>
<a class="btn btn-outline-secondary btn-sm" href="{{ url_for('library_item_invoices', item_id=e.item_id) }}">Rechnungen / Historie</a>
</div>
</td>
<td>
@@ -322,9 +322,9 @@
{% if e.invoice_number %}
<div class="mono">{{ e.invoice_number }}</div>
<div class="muted">{{ e.invoice_amount }}</div>
{% if e.invoice_corrections_count %}
<!--{% if e.invoice_corrections_count %}
<div class="muted" style="color:#7c2d12;">{{ e.invoice_corrections_count }} Korrektur(en)</div>
{% endif %}
{% endif %} -->
<div style="margin-top:6px;">
<a class="btn btn-outline-primary btn-sm" href="{{ url_for('admin_view_invoice_pdf', borrow_id=e.id) }}" target="_blank" rel="noopener">PDF öffnen</a>
</div>
@@ -363,13 +363,13 @@
<button type="button" class="btn btn-warning btn-sm" onclick="openRepairModal('{{ e.item_id }}', '{{ e.item_code }}')">Reparieren / Ersetzen</button>
{% endif %}
{% if e.invoice_number %}
<!--{% if e.invoice_number %}
<form method="post" action="{{ url_for('admin_add_invoice_correction', borrow_id=e.id) }}" onsubmit="return confirm('Korrekturbuchung hinzufügen?');" style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap;">
<input type="text" name="correction_reason" value="Korrektur zu {{ e.invoice_number }}" placeholder="Korrekturgrund" required style="padding:6px; border:1px solid #ddd; border-radius:6px; min-width:160px; max-width:180px;">
<input type="text" name="amount_delta" placeholder="z.B. -{{ e.invoice_amount }}" style="padding:6px; border:1px solid #ddd; border-radius:6px; width:120px;">
<button type="submit" class="btn btn-outline-danger btn-sm">Korrektur</button>
</form>
{% endif %}
{% endif %}-->
{% if e.status in ['active', 'planned'] %}
<form method="post" action="{{ url_for('admin_reset_borrowing', borrow_id=e.id) }}" onsubmit="return confirm('Ausleihe zurücksetzen?');">
+1 -1
View File
@@ -132,7 +132,7 @@
</div>
<div class="head-actions">
<a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Zur Bibliotheks-Ausleihenverwaltung</a>
<a class="btn btn-secondary" href="{{ url_for('library_admin') }}">Bibliothek öffnen</a>
<a class="btn btn-secondary" href="{{ url_for('library_view') }}">Bibliothek öffnen</a>
</div>
</div>
+225
View File
@@ -0,0 +1,225 @@
{% extends "base.html" %}
{% block title %}Mahnungsübersicht{% endblock %}
{% block content %}
<style>
.mahnungen-shell {
max-width: 1180px;
margin: 0 auto;
padding: 20px;
}
.mahnungen-head {
background: linear-gradient(135deg, #ffffff 0%, #f7f9fc 100%);
border: 1px solid #dbe4ee;
border-radius: 16px;
padding: 18px 20px;
margin-bottom: 20px;
box-shadow: 0 10px 26px rgba(15, 23, 42, 0.07);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 16px;
}
.mahnungen-head h1 {
margin: 0;
font-size: 1.55rem;
color: #1e293b;
}
.head-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.search-box-wrapper {
margin-bottom: 16px;
}
.search-box {
width: 100%;
max-width: 360px;
padding: 10px 14px;
border: 1px solid #cbd5e1;
border-radius: 8px;
font-size: 0.95rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.search-box:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
}
.mahnungen-card {
background: var(--ui-surface, #ffffff);
border: 1px solid #e2e8f0;
border-radius: 16px;
padding: 18px;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.06);
}
.mahnungen-table {
width: 100%;
border-collapse: collapse;
}
.mahnungen-table th,
.mahnungen-table td {
padding: 12px 10px;
border-bottom: 1px solid #edf2f7;
vertical-align: middle;
text-align: left;
}
.mahnungen-table th {
font-size: 0.83rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #64748b;
background: var(--ui-surface-soft, #f8fafc);
}
.mahnungen-table tr:hover td {
background: #fbfdff;
}
.badge-pill {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 999px;
font-size: 0.78rem;
font-weight: 700;
line-height: 1;
}
.badge-open { background: #fee2e2; color: #991b1b; }
.badge-warning { background: #fef3c7; color: #b45309; }
.badge-info { background: #e0f2fe; color: #0369a1; }
.badge-paid { background: #dcfce7; color: #166534; }
.mono {
font-family: monospace;
font-size: 0.92rem;
}
.text-danger {
color: #dc2626;
font-weight: 700;
}
.empty-state {
text-align: center;
color: #6b7280;
padding: 36px 20px;
}
@media (max-width: 900px) {
.mahnungen-table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
}
</style>
<div class="mahnungen-shell">
<div class="mahnungen-head">
<h1>Mahnungsübersicht</h1>
<div class="head-actions">
<a class="btn btn-outline-secondary" href="{{ url_for('library_loans_admin') }}">Zur Ausleihenverwaltung</a>
<a class="btn btn-secondary" href="{{ url_for('library_view') }}">Bibliothek öffnen</a>
</div>
</div>
<div class="mahnungen-card">
{% if overdue_list %}
<div class="search-box-wrapper">
<input type="text" id="searchInput" class="search-box" placeholder="🔍 Suche nach Nutzer, Klasse oder Gegenstand..." onkeyup="filterTable()">
</div>
<table class="mahnungen-table" id="mahnungTable">
<thead>
<tr>
<th>Nutzer</th>
<th>Klasse</th>
<th>Gegenstand</th>
<th>Fällig am</th>
<th>Tage drüber</th>
<th>Mahnstufe</th>
<th>Konto-Status</th>
</tr>
</thead>
<tbody>
{% for item in overdue_list %}
<tr>
<td><strong>{{ item.user or '—' }}</strong></td>
<td>{{ item.klasse or '—' }}</td>
<td>{{ item.item_name or '—' }}</td>
<td><span class="mono">{{ item.due_date or '—' }}</span></td>
<td class="text-danger">{{ item.days_overdue }} Tage</td>
<td>
{% if item.mahnstufe == 2 %}
<span class="badge-pill badge-open">Stufe 2 (Gesperrt)</span>
{% elif item.mahnstufe == 1 %}
<span class="badge-pill badge-warning">Stufe 1 (Gewarnt)</span>
{% else %}
<span class="badge-pill badge-info">Stufe 0 (Neu)</span>
{% endif %}
</td>
<td>
{% if item.is_blocked %}
<span class="badge-pill badge-open">Gesperrt</span>
{% else %}
<span class="badge-pill badge-paid">Aktiv</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<strong>Hervorragend!</strong> Aktuell gibt es keine überfälligen Ausleihen im System.
</div>
{% endif %}
</div>
</div>
<script>
function filterTable() {
const input = document.getElementById("searchInput");
const filter = input.value.toLowerCase();
const table = document.getElementById("mahnungTable");
const tr = table.getElementsByTagName("tbody")[0].getElementsByTagName("tr");
for (let i = 0; i < tr.length; i++) {
const tdUser = tr[i].getElementsByTagName("td")[0];
const tdClass = tr[i].getElementsByTagName("td")[1];
const tdItem = tr[i].getElementsByTagName("td")[2];
if (tdUser || tdClass || tdItem) {
const textUser = tdUser.textContent || tdUser.innerText;
const textClass = tdClass.textContent || tdClass.innerText;
const textItem = tdItem.textContent || tdItem.innerText;
if (
textUser.toLowerCase().indexOf(filter) > -1 ||
textClass.toLowerCase().indexOf(filter) > -1 ||
textItem.toLowerCase().indexOf(filter) > -1
) {
tr[i].style.display = "";
} else {
tr[i].style.display = "none";
}
}
}
}
</script>
{% endblock %}
+15 -15
View File
@@ -1531,22 +1531,22 @@
}
const isbnField = document.getElementById('isbn');
const infoContainer = document.getElementById('book-info-container');
if (!isbnField || !infoContainer) {
alert('Fehler: Erforderliche Formularelemente nicht gefunden.');
return;
}
const isbn = normalizeIsbnClient(isbnField.value);
if (!isbn) {
infoContainer.innerHTML = '<div class="error-message">Bitte geben Sie eine ISBN oder einen Barcode ein.</div>';
return;
}
// Show loading indicator
infoContainer.innerHTML = '<div class="loading-spinner">Informationen werden abgerufen...</div>';
// Make API request to fetch book data
fetch(`/fetch_book_info/${isbn}`)
.then(response => {
@@ -1564,10 +1564,10 @@
infoContainer.innerHTML = `<div class="error-message">${data.error}</div>`;
return;
}
// Store book data globally for import functionality
currentBookData = data;
// Display book information with import button
infoContainer.innerHTML = `
<div class="book-info">
@@ -1602,23 +1602,23 @@
alert('Keine Buchdaten verfügbar zum Import.');
return;
}
// Get form fields
const nameField = document.getElementById('name');
const descriptionField = document.getElementById('beschreibung');
const priceField = document.getElementById('anschaffungskosten'); // <-- NEU
if (!nameField || !descriptionField) {
alert('Fehler: Formularfelder nicht gefunden.');
return;
}
// Generate book title
let bookTitle = currentBookData.title || '';
if (currentBookData.authors && currentBookData.authors !== 'Unknown Author') {
bookTitle += ` - ${currentBookData.authors}`;
}
// Generate description
let description = '';
if (currentBookData.description && currentBookData.description !== 'No description available') {
@@ -1639,11 +1639,11 @@
description += `\nSeiten: ${currentBookData.pageCount}`;
}
}
// Import the data into form fields
nameField.value = bookTitle;
descriptionField.value = description;
// Preis in das Formularfeld eintragen
if (priceField && currentBookData.price !== null && currentBookData.price !== undefined) {
// Wandelt den Float (z.B. 12.25) in einen String mit Komma (12,25) um
@@ -1655,14 +1655,14 @@
if (currentBookData.thumbnail) {
downloadBookCover(currentBookData.thumbnail);
}
// Show success message
const infoContainer = document.getElementById('book-info-container');
const successMessage = document.createElement('div');
successMessage.className = 'import-success-message';
successMessage.textContent = 'Buchdaten erfolgreich in das Formular übernommen!';
infoContainer.appendChild(successMessage);
// Remove success message after 3 seconds
setTimeout(() => {
if (successMessage.parentNode) {