|
|
|
@@ -2150,9 +2150,8 @@ def generate_ausweis_id_excel(existing_ids_set):
|
|
|
|
|
else:
|
|
|
|
|
print(f"Already found: {new_id}, trying another...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _upload_student_cards_excel():
|
|
|
|
|
"""Bulk import student cards from Excel with automatic name/class mapping."""
|
|
|
|
|
"""Bulk import student cards from Excel with automatic name/class mapping, rollover support, and encryption."""
|
|
|
|
|
if 'username' not in session:
|
|
|
|
|
flash('Nicht angemeldet.', 'error')
|
|
|
|
|
return redirect(url_for('login'))
|
|
|
|
@@ -2177,6 +2176,8 @@ def _upload_student_cards_excel():
|
|
|
|
|
flash('Nur .xlsx oder .csv Dateien werden unterstützt.', 'error')
|
|
|
|
|
return redirect(url_for('student_cards_admin'))
|
|
|
|
|
|
|
|
|
|
rollover_mode = request.form.get('rollover_mode') in ['true', '1', 'on']
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
header_row, data_rows = _load_tabular_upload(excel_file)
|
|
|
|
|
except Exception as exc:
|
|
|
|
@@ -2191,12 +2192,14 @@ def _upload_student_cards_excel():
|
|
|
|
|
|
|
|
|
|
synonyms = {
|
|
|
|
|
'ausweis_id': ['ausweis_id', 'ausweisid', 'ausweis-id', 'karte', 'kartennummer', 'card_id', 'id'],
|
|
|
|
|
'ausweis_ident': ['lokales Differenzierungsmerkmal', 'lokales differenzierungsmerkmal', 'lokales_differenzierungsmerkmal', 'ausweis_ident', 'differenzierungsmerkmal'],
|
|
|
|
|
'first_name': ['vorname', 'first_name', 'firstname', 'rufname', 'Vorname'],
|
|
|
|
|
'last_name': ['nachname', 'last_name', 'lastname', 'Nachname'],
|
|
|
|
|
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse', 'Jahrgang', 'Klasse'],
|
|
|
|
|
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise', 'Notizen', 'Bemerkung', 'Hinweis', 'Hinweise'],
|
|
|
|
|
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage', 'max_borrow_days', 'Ausleihdauer'],
|
|
|
|
|
'ausweis_ident': ['lokales differenzierungsmerkmal', 'lokales_differenzierungsmerkmal', 'ausweis_ident',
|
|
|
|
|
'differenzierungsmerkmal'],
|
|
|
|
|
'first_name': ['vorname', 'first_name', 'firstname', 'rufname'],
|
|
|
|
|
'last_name': ['nachname', 'last_name', 'lastname'],
|
|
|
|
|
'class_name': ['klasse', 'class', 'class_name', 'jahrgang', 'jahrgangsstufe', 'stufe', 'gruppe', 'asv_klasse'],
|
|
|
|
|
'notes': ['notizen', 'notes', 'bemerkungen', 'bemerkung', 'hinweis', 'hinweise'],
|
|
|
|
|
'default_borrow_days': ['standard_ausleihdauer', 'ausleihdauer', 'borrow_days', 'tage', 'leihtage',
|
|
|
|
|
'max_borrow_days'],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def col_index(key):
|
|
|
|
@@ -2227,8 +2230,9 @@ def _upload_student_cards_excel():
|
|
|
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
|
|
|
try:
|
|
|
|
|
db = client[cfg.MONGODB_DB]
|
|
|
|
|
|
|
|
|
|
student_cards_cursor = db['student_cards'].find({}, {'AusweisId': 1})
|
|
|
|
|
student_cards_col = db['student_cards']
|
|
|
|
|
|
|
|
|
|
student_cards_cursor = student_cards_col.find({}, {'AusweisId': 1})
|
|
|
|
|
existing_ids.update(
|
|
|
|
|
str(card.get('AusweisId', '')).strip().upper()
|
|
|
|
|
for card in student_cards_cursor
|
|
|
|
@@ -2250,34 +2254,27 @@ def _upload_student_cards_excel():
|
|
|
|
|
|
|
|
|
|
ausweis_id = sanitize_form_value(val('ausweis_id'))
|
|
|
|
|
ausweis_ident = sanitize_form_value(val('ausweis_ident'))
|
|
|
|
|
class_name = sanitize_form_value(val('class_name'))
|
|
|
|
|
class_name = sanitize_form_value(val('class_name')) or ""
|
|
|
|
|
notes = sanitize_form_value(val('notes'))
|
|
|
|
|
|
|
|
|
|
# Ausleihdauer extrahieren und standardmäßig auf 14 setzen
|
|
|
|
|
default_borrow_days = _excel_int(val('default_borrow_days'))
|
|
|
|
|
if not default_borrow_days:
|
|
|
|
|
default_borrow_days = 14
|
|
|
|
|
|
|
|
|
|
# Vor- und Nachname sicher auslesen und zusammensetzen
|
|
|
|
|
default_borrow_days = _excel_int(val('default_borrow_days')) or 14
|
|
|
|
|
|
|
|
|
|
first_name = sanitize_form_value(val('first_name')) or ""
|
|
|
|
|
last_name = sanitize_form_value(val('last_name')) or ""
|
|
|
|
|
student_name = f"{first_name} {last_name}".strip()
|
|
|
|
|
|
|
|
|
|
# Leere Zeilen überspringen
|
|
|
|
|
if not ausweis_id and not student_name and not class_name:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
row_errors = []
|
|
|
|
|
|
|
|
|
|
if not student_name:
|
|
|
|
|
row_errors.append('Vorname und Nachname fehlen')
|
|
|
|
|
|
|
|
|
|
# Logik für die Haupt-AusweisID
|
|
|
|
|
if not ausweis_id and student_name:
|
|
|
|
|
ausweis_id = generate_ausweis_id(existing_ids)
|
|
|
|
|
validation_warnings.append((row_number, f'Ausweis-ID wurde automatisch erzeugt: {ausweis_id}'))
|
|
|
|
|
existing_ids.add(ausweis_id.upper())
|
|
|
|
|
elif ausweis_id:
|
|
|
|
|
existing_ids.add(ausweis_id.upper())
|
|
|
|
|
elif ausweis_id and not rollover_mode:
|
|
|
|
|
ausweis_id = str(ausweis_id).strip().upper()
|
|
|
|
|
if ausweis_id in existing_ids:
|
|
|
|
|
row_errors.append(f'Ausweis-ID {ausweis_id} existiert bereits')
|
|
|
|
@@ -2300,29 +2297,92 @@ def _upload_student_cards_excel():
|
|
|
|
|
'notes': notes,
|
|
|
|
|
'default_borrow_days': default_borrow_days,
|
|
|
|
|
})
|
|
|
|
|
finally:
|
|
|
|
|
client.close()
|
|
|
|
|
|
|
|
|
|
if validation_errors:
|
|
|
|
|
details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_errors[:15]])
|
|
|
|
|
flash(f'Validierung fehlgeschlagen ({len(validation_errors)} Zeilen). {details}', 'error')
|
|
|
|
|
return redirect(url_for('student_cards_admin'))
|
|
|
|
|
if validation_errors:
|
|
|
|
|
details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_errors[:15]])
|
|
|
|
|
flash(f'Validierung fehlgeschlagen ({len(validation_errors)} Zeilen). {details}', 'error')
|
|
|
|
|
return redirect(url_for('student_cards_admin'))
|
|
|
|
|
|
|
|
|
|
if validation_only:
|
|
|
|
|
warning_text = ''
|
|
|
|
|
if validation_warnings:
|
|
|
|
|
warning_details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])
|
|
|
|
|
warning_text = f' Hinweise: {warning_details}'
|
|
|
|
|
flash(f'Validierung erfolgreich: {len(planned_rows)} Ausweise würden importiert.{warning_text}', 'success')
|
|
|
|
|
return redirect(url_for('student_cards_admin'))
|
|
|
|
|
matched_db_doc_ids = set()
|
|
|
|
|
rows_to_create = []
|
|
|
|
|
matched_count = 0
|
|
|
|
|
|
|
|
|
|
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
|
|
|
|
try:
|
|
|
|
|
db = client[cfg.MONGODB_DB]
|
|
|
|
|
student_cards = db['student_cards']
|
|
|
|
|
if rollover_mode:
|
|
|
|
|
raw_db_cards = list(student_cards_col.find())
|
|
|
|
|
decrypted_db_cards = []
|
|
|
|
|
|
|
|
|
|
for doc in raw_db_cards:
|
|
|
|
|
dec_name = decrypt_text(doc.get('SchülerName')) if doc.get('SchülerName') else ""
|
|
|
|
|
dec_class = decrypt_text(doc.get('Klasse')) if doc.get('Klasse') else ""
|
|
|
|
|
|
|
|
|
|
raw_ident = doc.get('ausweis_ident') or doc.get('AusweisIdent') or ""
|
|
|
|
|
dec_ident = raw_ident
|
|
|
|
|
if raw_ident and str(raw_ident).startswith("gAAAAA"):
|
|
|
|
|
try:
|
|
|
|
|
dec_ident = decrypt_text(raw_ident)
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
decrypted_db_cards.append({
|
|
|
|
|
'_id': doc['_id'],
|
|
|
|
|
'AusweisId': str(doc.get('AusweisId', '')).strip().upper(),
|
|
|
|
|
'AusweisIdent': str(dec_ident or '').strip().upper(),
|
|
|
|
|
'SchülerName': str(dec_name or '').strip().lower(),
|
|
|
|
|
'Klasse': str(dec_class or '').strip().lower(),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for excel_row in planned_rows:
|
|
|
|
|
ex_ident = str(excel_row['ausweis_ident'] or '').strip().upper()
|
|
|
|
|
ex_name = str(excel_row['student_name'] or '').strip().lower()
|
|
|
|
|
ex_class = str(excel_row['class_name'] or '').strip().lower()
|
|
|
|
|
|
|
|
|
|
match_found = None
|
|
|
|
|
for db_card in decrypted_db_cards:
|
|
|
|
|
if db_card['_id'] in matched_db_doc_ids:
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
ident_matches = ex_ident and (ex_ident == db_card['AusweisIdent'])
|
|
|
|
|
secondary_matches = (ex_name and ex_name == db_card['SchülerName']) and \
|
|
|
|
|
(ex_class and ex_class == db_card['Klasse'])
|
|
|
|
|
|
|
|
|
|
if ident_matches or secondary_matches:
|
|
|
|
|
match_found = db_card
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if match_found:
|
|
|
|
|
matched_db_doc_ids.add(match_found['_id'])
|
|
|
|
|
matched_count += 1
|
|
|
|
|
else:
|
|
|
|
|
rows_to_create.append(excel_row)
|
|
|
|
|
|
|
|
|
|
db_ids_to_delete = [
|
|
|
|
|
doc['_id'] for doc in raw_db_cards
|
|
|
|
|
if doc['_id'] not in matched_db_doc_ids
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
rows_to_create = planned_rows
|
|
|
|
|
db_ids_to_delete = []
|
|
|
|
|
|
|
|
|
|
if validation_only:
|
|
|
|
|
warning_text = f" Hinweise: {'; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])}" if validation_warnings else ""
|
|
|
|
|
if rollover_mode:
|
|
|
|
|
flash(
|
|
|
|
|
f'Validierung erfolgreich ({len(planned_rows)} Excel-Zeilen). '
|
|
|
|
|
f'Abgleich-Vorschau: {matched_count} unverändert, {len(rows_to_create)} neu, {len(db_ids_to_delete)} zum Löschen.{warning_text}',
|
|
|
|
|
'success'
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
flash(f'Validierung erfolgreich: {len(planned_rows)} Ausweise würden importiert.{warning_text}',
|
|
|
|
|
'success')
|
|
|
|
|
return redirect(url_for('student_cards_admin'))
|
|
|
|
|
|
|
|
|
|
deleted_count = 0
|
|
|
|
|
if db_ids_to_delete:
|
|
|
|
|
res = student_cards_col.delete_many({'_id': {'$in': db_ids_to_delete}})
|
|
|
|
|
deleted_count = res.deleted_count
|
|
|
|
|
|
|
|
|
|
created_total = 0
|
|
|
|
|
for row in planned_rows:
|
|
|
|
|
for row in rows_to_create:
|
|
|
|
|
encrypted_payload = encrypt_document_fields(
|
|
|
|
|
{
|
|
|
|
|
'ausweis_ident': row['ausweis_ident'],
|
|
|
|
@@ -2332,29 +2392,33 @@ def _upload_student_cards_excel():
|
|
|
|
|
},
|
|
|
|
|
STUDENT_CARD_ENCRYPTED_FIELDS
|
|
|
|
|
)
|
|
|
|
|
student_cards.insert_one({
|
|
|
|
|
student_cards_col.insert_one({
|
|
|
|
|
'AusweisId': row['ausweis_id'],
|
|
|
|
|
'StandardAusleihdauer': int(row['default_borrow_days']),
|
|
|
|
|
'Erstellt': datetime.datetime.now(),
|
|
|
|
|
**encrypted_payload,
|
|
|
|
|
})
|
|
|
|
|
created_total += 1
|
|
|
|
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
app.logger.error(f'Error importing student cards from Excel: {exc}')
|
|
|
|
|
flash(f'Fehler beim Import der Bibliotheksausweise', 'error')
|
|
|
|
|
flash('Fehler beim Import der Bibliotheksausweise.', 'error')
|
|
|
|
|
return redirect(url_for('student_cards_admin'))
|
|
|
|
|
finally:
|
|
|
|
|
client.close()
|
|
|
|
|
|
|
|
|
|
if validation_warnings:
|
|
|
|
|
warning_details = '; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])
|
|
|
|
|
flash(f'Excel-Import erfolgreich: {created_total} Ausweise importiert. Hinweise: {warning_details}', 'warning')
|
|
|
|
|
warning_details = f" Hinweise: {'; '.join([f'Zeile {n}: {msg}' for n, msg in validation_warnings[:10]])}" if validation_warnings else ""
|
|
|
|
|
if rollover_mode:
|
|
|
|
|
flash(
|
|
|
|
|
f'Schuljahres-Abgleich erfolgreich: {matched_count} Schüler beibehalten, '
|
|
|
|
|
f'{created_total} neu hinzugefügt, {deleted_count} alte Einträge gelöscht.{warning_details}',
|
|
|
|
|
'success'
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
flash(f'Excel-Import erfolgreich: {created_total} Ausweise importiert.', 'success')
|
|
|
|
|
flash(f'Excel-Import erfolgreich: {created_total} Ausweise importiert.{warning_details}', 'success')
|
|
|
|
|
|
|
|
|
|
return redirect(url_for('student_cards_admin'))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _upload_excel_items(scope='inventory'):
|
|
|
|
|
"""Bulk import inventory/library items from Excel with validation-first workflow."""
|
|
|
|
|
if 'username' not in session:
|
|
|
|
@@ -3262,9 +3326,6 @@ def library_loans_admin():
|
|
|
|
|
|
|
|
|
|
_ensure_audit_indexes_once()
|
|
|
|
|
|
|
|
|
|
# IMPORT HINZUGEFÜGT: Entschlüsselungs-Tool importieren
|
|
|
|
|
from modules.inventarsystem.data_protection import decrypt_text
|
|
|
|
|
|
|
|
|
|
def fmt_dt(dt):
|
|
|
|
|
try:
|
|
|
|
|
return dt.strftime('%d.%m.%Y %H:%M') if dt else ''
|
|
|
|
|