Compare commits

...

13 Commits

Author SHA1 Message Date
Aiirondev_dev 9e77b4604d slight database mixup fix
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-19 19:57:13 +02:00
Aiirondev_dev c06473644a slight database mixup fix
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-19 18:55:30 +02:00
Aiirondev_dev 6c855ed9d9 slight database mixup fix
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-19 18:49:36 +02:00
Aiirondev_dev b09a6f7720 slight database mixup fix
Release Inventarsystem / release-docker (push) Successful in 3m25s
2026-08-19 18:42:01 +02:00
Aiirondev_dev 8c5185bd8c Spelling mistakes and renaming done.
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-18 21:52:45 +02:00
Aiirondev_dev 0b30f8463f Renamed from Ausleihen, to Alle Ausleihen. in base.html
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-18 21:43:00 +02:00
Aiirondev_dev 671a9e8e85 Renaming of the ResetCardBtn to Feld zurücksetzen in regards to user comprehension.
Release Inventarsystem / release-docker (push) Successful in 2m26s
2026-08-18 21:31:52 +02:00
Aiirondev_dev 3d9e5c470a Fix of an issue with the data being use for the button insert
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-18 21:12:47 +02:00
Aiirondev_dev e636242542 fix for the cost button in the damaged items cost
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-18 21:00:14 +02:00
Aiirondev_dev d61aeebb8f Implementation of a extra button to add the complete replacement for the Bibliotheks item
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-18 20:37:04 +02:00
Aiirondev_dev 375e9c46eb slight fix of an import dubbeling
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-18 20:33:17 +02:00
Aiirondev 8dc773d202 README.md aktualisiert
Release Inventarsystem / release-docker (push) Successful in 2m45s
2026-08-18 17:59:25 +00:00
Aiirondev_dev 1b2b462c52 changes to the _upload student to have the field encryptet
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-18 18:51:49 +02:00
5 changed files with 125 additions and 85 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Inventarsystem
[![https://github.com/AIIrondev/legendary-octo-garbanzo](https://github.com/AIIrondev/legendary-octo-garbanzo/actions/workflows/release-docker.yml/badge.svg)](https://github.com/AIIrondev/legendary-octo-garbanzo/actions/workflows/release-docker.yml)
[![https://git.invario-software.eu/Invario/Inventarsystem](https://git.invario-software.eu/Invario/Inventarsystem/actions/workflows/release-docker.yml/badge.svg)](https://git.invario-software.eu/Invario/Inventarsystem/actions/workflows/release-docker.yml)
[![wakatime](https://wakatime.com/badge/user/30b8509f-5e17-4d16-b6b8-3ca0f3f936d3/project/8a380b7f-389f-4a7e-8877-0fe9e1a4c243.svg)](https://wakatime.com/badge/user/30b8509f-5e17-4d16-b6b8-3ca0f3f936d3/project/8a380b7f-389f-4a7e-8877-0fe9e1a4c243)
+61 -48
View File
@@ -2151,14 +2151,15 @@ def generate_ausweis_id_excel(existing_ids_set):
print(f"Already found: {new_id}, trying another...")
def _upload_student_cards_excel():
"""Bulk import student cards with optional school year rollover (Abgleich)."""
"""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'))
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.', 'error')
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library_view'))
if not cfg.MODULES.is_enabled('student_cards'):
@@ -2175,7 +2176,6 @@ 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:
@@ -2220,7 +2220,7 @@ def _upload_student_cards_excel():
}
validation_only = (request.form.get('excel_action') or '').strip().lower() == 'validate'
max_rows = 15000
max_rows = 1500
planned_rows = []
validation_errors = []
@@ -2230,11 +2230,14 @@ def _upload_student_cards_excel():
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try:
db = client[cfg.MONGODB_DB]
raw_db_cards = list(db['student_cards'].find())
student_cards_col = db['student_cards']
for card in raw_db_cards:
if card.get('AusweisId'):
existing_ids.add(str(card.get('AusweisId')).strip().upper())
student_cards_cursor = student_cards_col.find({}, {'AusweisId': 1})
existing_ids.update(
str(card.get('AusweisId', '')).strip().upper()
for card in student_cards_cursor
if card.get('AusweisId')
)
processed_rows = 0
for row_number, row_values in enumerate(data_rows, start=2):
@@ -2267,6 +2270,20 @@ def _upload_student_cards_excel():
if not student_name:
row_errors.append('Vorname und Nachname fehlen')
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 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')
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
@@ -2276,8 +2293,6 @@ 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,
@@ -2293,16 +2308,19 @@ def _upload_student_cards_excel():
matched_count = 0
if rollover_mode:
raw_db_cards = list(student_cards_col.find())
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"):
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 = dp.decrypt_text(dec_ident)
except:
dec_ident = decrypt_text(raw_ident)
except Exception:
pass
decrypted_db_cards.append({
@@ -2319,12 +2337,11 @@ def _upload_student_cards_excel():
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'])
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'])
@@ -2347,15 +2364,18 @@ def _upload_student_cards_excel():
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'
)
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'))
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}})
@@ -2363,18 +2383,9 @@ def _upload_student_cards_excel():
created_total = 0
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'],
@@ -2382,8 +2393,7 @@ def _upload_student_cards_excel():
STUDENT_CARD_ENCRYPTED_FIELDS
)
student_cards_col.insert_one({
'AusweisId': row_ausweis_id,
'AusweisIdent': row_ausweis_ident,
'AusweisId': row['ausweis_id'],
'StandardAusleihdauer': int(row['default_borrow_days']),
'Erstellt': datetime.datetime.now(),
**encrypted_payload,
@@ -2391,20 +2401,21 @@ def _upload_student_cards_excel():
created_total += 1
except Exception as exc:
app.logger.error(f'Error importing student cards: {exc}')
flash('Fehler beim Verarbeiten der Bibliotheksausweise.', 'error')
app.logger.error(f'Error importing student cards from Excel: {exc}')
flash('Fehler beim Import der Bibliotheksausweise.', 'error')
return redirect(url_for('student_cards_admin'))
finally:
client.close()
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.',
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'))
@@ -3315,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 ''
@@ -3374,6 +3382,7 @@ def library_loans_admin():
'item_code': item_doc.get('Code_4', ''),
'item_author': item_doc.get('Author', ''),
'item_isbn': item_doc.get('ISBN', ''),
'item_cost_raw': item_doc.get('Anschaffungskosten', ''),
'user': decrypted_user,
'status': record.get('Status', ''),
'start': fmt_dt(record.get('Start')),
@@ -9105,7 +9114,7 @@ def library_item_invoices(item_id):
flash('Bibliotheks-Modul ist deaktiviert.', 'error')
return redirect(url_for('home_admin'))
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['pages'].get('library_loans_admin', False):
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
return redirect(url_for('library_view'))
@@ -9126,9 +9135,14 @@ def library_item_invoices(item_id):
flash('Bibliotheksmedium nicht gefunden.', 'error')
return redirect(url_for('library_loans_admin'))
# Hole die ID sowohl als ObjectId als auch als String
item_id_obj = item_doc.get('_id')
item_id_str = str(item_id_obj)
borrow_docs = list(ausleihungen.find(
{
'Item': str(item_doc.get('_id')),
# $in sucht nach Treffern, egal ob die ID als String oder ObjectId in der DB steht
'Item': {'$in': [item_id_str, item_id_obj]},
'InvoiceData': {'$exists': True, '$ne': {}}
},
{
@@ -9186,7 +9200,6 @@ def library_item_invoices(item_id):
if client:
client.close()
@app.route('/admin_reset_user_password', methods=['POST'])
def admin_reset_user_password():
"""
+3 -3
View File
@@ -1261,10 +1261,10 @@
<li><a class="dropdown-item" href="{{ url_for('admin_school_settings') }}">Schulstammdaten</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_borrowings', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Ausleihen</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin_borrowings') }}">Alle Ausleihen</a></li>
{% endif %}
{% if current_permissions.pages.get('admin_damaged_items', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Defekte Items</a></li>
<li><a class="dropdown-item" href="{{ url_for('admin_damaged_items') }}">Alle defekten Items</a></li>
{% endif %}
{% if current_permissions.actions.get('can_view_logs', False) and current_permissions.pages.get('admin_audit_dashboard', False) %}
<li><a class="dropdown-item" href="{{ url_for('admin_audit_dashboard') }}">Audit Dashboard</a></li>
@@ -1372,7 +1372,7 @@
{% if 'username' in session and current_permissions.actions.get('can_manage_settings', False) %}
<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') }}">Ausleihen / Defekte Items</a></li>
<li><a class="dropdown-item" href="{{ url_for('library_loans_admin') }}">Alle Ausleihen/Alle Defekten Items</a></li>
{% endif %}
{% if student_cards_module_enabled %}
{% if current_permissions.actions.get('can_manage_users', False) %}
+59 -32
View File
@@ -407,14 +407,14 @@
</div>
</div>
<div id="damage-invoice-modal" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
<div id="damage-invoice-modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" style="display:none; position:fixed; inset:0; background:rgba(15,23,42,0.72); z-index:9999; padding:20px; overflow:auto;">
<div style="max-width:760px; margin:40px auto; background: var(--ui-surface); border-radius:12px; padding:24px; box-shadow:0 20px 60px rgba(0,0,0,0.3);">
<div style="display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:18px;">
<div>
<h2 style="margin:0;">Rechnung erstellen</h2>
<h2 id="modal-title" style="margin:0;">Rechnung erstellen</h2>
<p style="margin:6px 0 0; color:#666;">Die Rechnung nutzt das bestehende Rechnungssystem und kann direkt nach der Schadensmeldung erstellt werden.</p>
</div>
<button type="button" class="btn btn-secondary" onclick="closeDamageInvoiceModal()">Schließen</button>
<button type="button" class="btn btn-secondary" onclick="closeDamageInvoiceModal()" aria-label="Modal schließen">Schließen</button>
</div>
<form id="damage-invoice-form" method="post" action="">
@@ -432,7 +432,10 @@
<input id="damage-invoice-code" type="text" readonly style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px; background: var(--ui-surface-soft);">
</div>
<div>
<label for="damage-invoice-amount" style="display:block; font-weight:700; margin-bottom:6px;">Preis</label>
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:6px;">
<label for="damage-invoice-amount" style="font-weight:700; margin:0;">Preis</label>
<button type="button" id="damage-invoice-replace-btn" class="btn btn-outline-secondary btn-sm" style="padding: 2px 8px; font-size: 0.75rem;">Komplett ersetzen</button>
</div>
<input id="damage-invoice-amount" name="invoice_amount" type="text" required style="width:100%; padding:10px; border:1px solid #ddd; border-radius:6px;" placeholder="z.B. 12,50">
</div>
</div>
@@ -477,6 +480,7 @@
const damageInvoiceCode = document.getElementById('damage-invoice-code');
const damageInvoiceAmount = document.getElementById('damage-invoice-amount');
const damageInvoiceReason = document.getElementById('damage-invoice-reason');
const damageInvoiceReplaceBtn = document.getElementById('damage-invoice-replace-btn');
function openDamageReportPrompt(button) {
const row = button.closest('.loan-row');
@@ -494,41 +498,48 @@
const description = noteInput.trim() || 'Schaden erneut gemeldet';
// Visuelles Feedback: Button deaktivieren und Text ändern
const originalText = button.textContent;
button.disabled = true;
button.textContent = 'Speichere...';
fetch(`/report_damage/${itemId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description })
})
.then(response => response.json().then(data => ({ ok: response.ok, data })))
.then(({ ok, data }) => {
if (!ok || !data.success) {
.then(async response => {
// Robustes JSON-Parsing (verhindert Absturz, falls der Server kein JSON zurückgibt)
const data = await response.json().catch(() => ({}));
if (!response.ok || !data.success) {
throw new Error(data.message || 'Fehler beim Speichern der Schadensmeldung.');
}
return data;
})
.then(data => {
if (confirm('Schaden gespeichert. Soll direkt eine Rechnung erstellt werden?')) {
openDamageInvoiceModal(row, description);
// Button wieder zurücksetzen, da die Seite nicht neu geladen wird
button.disabled = false;
button.textContent = originalText;
return;
}
// Bei "Abbrechen" im Confirm -> Neuladen der Tabelle
window.location.reload();
})
.catch(error => {
alert(error.message || 'Fehler beim Speichern der Schadensmeldung.');
alert(error.message || 'Ein unbekannter Fehler ist aufgetreten.');
// Fehlerbehandlung: Button wieder aktiv schalten
button.disabled = false;
button.textContent = originalText;
});
}
window.openDamageReportPrompt = openDamageReportPrompt;
function openDamageInvoiceModal(row, description) {
const modal = document.getElementById('damage-invoice-modal');
const form = document.getElementById('damage-invoice-form');
const inputItem = document.getElementById('damage-invoice-item');
const inputBorrower = document.getElementById('damage-invoice-borrower');
const inputCode = document.getElementById('damage-invoice-code');
const inputAmount = document.getElementById('damage-invoice-amount');
const inputReason = document.getElementById('damage-invoice-reason');
if (!modal || !form) {
if (!damageInvoiceModal || !damageInvoiceForm) {
console.error("Modal oder Formular nicht gefunden.");
return;
}
@@ -537,29 +548,47 @@
const itemName = row.dataset.itemName || '';
const borrower = row.dataset.userName || '';
const itemCode = row.dataset.itemCode || '';
// KORREKTUR: Jetzt greifen wir auf das richtige dataset-Attribut zu
const itemCost = row.dataset.itemCost || '';
form.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
damageInvoiceForm.action = "{{ url_for('admin_create_invoice', borrow_id='__BORROW_ID__') }}".replace('__BORROW_ID__', borrowId);
inputItem.value = itemName;
inputBorrower.value = borrower;
inputCode.value = itemCode;
damageInvoiceItem.value = itemName;
damageInvoiceBorrower.value = borrower;
damageInvoiceCode.value = itemCode;
inputAmount.value = String(itemCost).replace(' EUR', '').trim();
// Feld zunächst leeren, damit der Ersetzen-Button genutzt werden kann
damageInvoiceAmount.value = '';
inputReason.value = description || `Schaden gemeldet für ${itemName}`;
// Original-Preis im Button als data-Attribut hinterlegen
if (damageInvoiceReplaceBtn) {
damageInvoiceReplaceBtn.dataset.acquisition_costs = String(itemCost).replace(' EUR', '').trim();
}
modal.style.display = 'block';
inputAmount.focus();
damageInvoiceReason.value = description || `Schaden gemeldet für ${itemName}`;
damageInvoiceModal.style.display = 'block';
// Accessibility: Fokus ins erste aktivierbare Feld setzen
damageInvoiceAmount.focus();
}
function closeDamageInvoiceModal() {
const modal = document.getElementById('damage-invoice-modal');
if (modal) {
modal.style.display = 'none';
if (damageInvoiceModal) {
damageInvoiceModal.style.display = 'none';
}
}
// Event-Listener für den Ersetzen-Button
if (damageInvoiceReplaceBtn) {
damageInvoiceReplaceBtn.addEventListener('click', function() {
if (this.dataset.acquisition_costs) {
damageInvoiceAmount.value = this.dataset.acquisition_costs;
}
});
}
window.openDamageInvoiceModal = openDamageInvoiceModal;
window.closeDamageInvoiceModal = closeDamageInvoiceModal;
@@ -571,8 +600,6 @@
});
}
window.closeDamageInvoiceModal = closeDamageInvoiceModal;
function applyFilters() {
const search = (searchInput.value || '').trim().toLowerCase();
const status = statusFilter.value;
@@ -613,4 +640,4 @@
applyFilters();
})();
</script>
{% endblock %}
{% endblock %}
+1 -1
View File
@@ -516,7 +516,7 @@
</select>
<input type="text" id="activeStudentCard" placeholder="Aktiver Ausweis (gescannt)">
<input type="text" id="manualItemCode" placeholder="Manueller Mediencode (optional)" style="min-width:180px;">
<button id="resetCardBtn" class="button" type="button">Ausweis löschen</button>
<button id="resetCardBtn" class="button" type="button">Feld zurücksetzen</button>
<button id="toggleScannerBtn" class="button" type="button">Scanner starten</button>
<label style="display:flex; align-items:center; gap:8px; margin-left:6px;">
<input type="checkbox" id="keyboardScannerToggle">