diff --git a/Web/app.py b/Web/app.py index 291b0b6..28b9f5c 100755 --- a/Web/app.py +++ b/Web/app.py @@ -2150,17 +2150,15 @@ 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 with optional school year rollover (Abgleich).""" if 'username' not in session: flash('Nicht angemeldet.', 'error') return redirect(url_for('login')) current_permissions = us.get_effective_permissions(session['username']) - if not current_permissions['actions'].get('can_manage_user', False): - flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') + flash('Ihnen fehlen die nötigen Berechtigungen.', 'error') return redirect(url_for('library_view')) if not cfg.MODULES.is_enabled('student_cards'): @@ -2177,6 +2175,9 @@ def _upload_student_cards_excel(): flash('Nur .xlsx oder .csv Dateien werden unterstützt.', 'error') return redirect(url_for('student_cards_admin')) + # CHECKBOX / SCHALTER: Schuljahres-Abgleich aktiviert? + 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): @@ -2217,7 +2220,7 @@ def _upload_student_cards_excel(): } validation_only = (request.form.get('excel_action') or '').strip().lower() == 'validate' - max_rows = 1500 + max_rows = 15000 planned_rows = [] validation_errors = [] @@ -2227,13 +2230,11 @@ 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}) - existing_ids.update( - str(card.get('AusweisId', '')).strip().upper() - for card in student_cards_cursor - if card.get('AusweisId') - ) + raw_db_cards = list(db['student_cards'].find()) + + for card in raw_db_cards: + if card.get('AusweisId'): + existing_ids.add(str(card.get('AusweisId')).strip().upper()) processed_rows = 0 for row_number, row_values in enumerate(data_rows, start=2): @@ -2250,43 +2251,22 @@ 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: - ausweis_id = str(ausweis_id).strip().upper() - if ausweis_id in existing_ids: - row_errors.append(f'Ausweis-ID {ausweis_id} existiert bereits') - else: - existing_ids.add(ausweis_id) - - if not ausweis_ident: - ausweis_ident = ausweis_id - if row_errors: validation_errors.append((row_number, '; '.join(row_errors))) continue @@ -2296,65 +2276,138 @@ def _upload_student_cards_excel(): 'ausweis_id': ausweis_id, 'ausweis_ident': ausweis_ident, 'student_name': student_name, + 'first_name': first_name, + 'last_name': last_name, 'class_name': class_name, '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: + decrypted_db_cards = [] + for doc in raw_db_cards: + dec_name = dp.decrypt_text(doc.get('SchülerName')) if doc.get('SchülerName') else "" + dec_class = dp.decrypt_text(doc.get('Klasse')) if doc.get('Klasse') else "" + dec_ident = doc.get('AusweisIdent') or "" + + if dec_ident and dec_ident.startswith("gAAAAA"): + try: + dec_ident = dp.decrypt_text(dec_ident) + except: + 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: + flash( + f'Validierung erfolgreich ({len(planned_rows)} Excel-Zeilen). ' + f'Abgleich: {matched_count} unverändert, {len(rows_to_create)} neu, {len(db_ids_to_delete)} zum Löschen.', + 'success' + ) + return redirect(url_for('student_cards_admin')) + + student_cards_col = db['student_cards'] + + 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: + row_ausweis_id = row['ausweis_id'] + if not row_ausweis_id: + row_ausweis_id = generate_ausweis_id(existing_ids) + existing_ids.add(row_ausweis_id.upper()) + + row_ausweis_ident = row['ausweis_ident'] + if not row_ausweis_ident: + random_chars = "".join(random.choices(string.ascii_uppercase + string.digits, k=5)) + row_ausweis_ident = f"LD-{random_chars}" + encrypted_payload = encrypt_document_fields( { - 'ausweis_ident': row['ausweis_ident'], 'SchülerName': row['student_name'], 'Klasse': row['class_name'], 'Notizen': row['notes'], }, STUDENT_CARD_ENCRYPTED_FIELDS ) - student_cards.insert_one({ - 'AusweisId': row['ausweis_id'], + student_cards_col.insert_one({ + 'AusweisId': row_ausweis_id, + 'AusweisIdent': row_ausweis_ident, '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') + app.logger.error(f'Error importing student cards: {exc}') + flash('Fehler beim Verarbeiten 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') + 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.', + 'success' + ) else: flash(f'Excel-Import erfolgreich: {created_total} Ausweise importiert.', '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: diff --git a/Web/templates/student_cards_admin.html b/Web/templates/student_cards_admin.html index 6af8f45..4114988 100644 --- a/Web/templates/student_cards_admin.html +++ b/Web/templates/student_cards_admin.html @@ -45,6 +45,33 @@ justify-content: space-between; } + .rollover-box { + background: #fff3cd; + border: 1px solid #ffeeba; + border-radius: 6px; + padding: 10px 12px; + margin-top: 4px; + } + + .rollover-label { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 13px; + color: #856404; + font-weight: 700; + margin: 0; + } + + .rollover-hint { + display: block; + margin-top: 4px; + font-size: 11px; + color: #856404; + line-height: 1.3; + } + .form-row { display: grid; grid-template-columns: 1fr 1fr; @@ -303,7 +330,7 @@ placeholder="z.B. Max Mustermann">
- + @@ -318,22 +345,22 @@
- +
- + + placeholder="z.B. xw5oo123bbe">
- + @@ -359,16 +386,29 @@
- +

Excel-Import

-

+

Laden Sie eine .xlsx- oder .csv-Datei hoch (z. B. aus ASV). Erkannt werden Name, Nachname, Klasse, Ausweis-ID, lokales Differenzierungsmerkmal, Notizen & Ausleihdauer.

+
+ + +
+ + + ⚠️ Warnung: Nicht mehr vorhandene Ausweise/Schüler werden beim Import entfernt bzw. abgeglichen. + +
+