diff --git a/Web/app.py b/Web/app.py index c2a545a..4e8216d 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6372,7 +6372,6 @@ def item_edit(id): """ Endpoint zum Laden und Aktualisieren eines Eintrags (item_edit). """ - # 1. Rechte- & Auth-Check if 'username' not in session: if request.method == 'POST' and request.is_json: return jsonify({'success': False, 'message': 'Nicht angemeldet.'}), 401 @@ -6392,7 +6391,6 @@ def item_edit(id): flash('Ungültige Element-ID.', 'error') return redirect(url_for('home_admin')) - # --- GET: Template anzeigen --- if request.method == 'GET': item = it.get_item(id) @@ -6401,10 +6399,24 @@ def item_edit(id): return redirect(url_for('home_admin')) item['_id'] = str(item['_id']) + + # Gruppen-Codes aufteilen in Basis-Code und Einzelcodes für die Textarea + base_code = item.get('Code_4', '') + individual_codes = [] + if item.get('SeriesGroupId'): + group_ids = it.get_group_item_ids(str(item['_id'])) + if group_ids: + for gid in group_ids: + g_item = it.get_item(gid) + c4 = g_item.get('Code_4') + if c4 and c4 != base_code: + individual_codes.append(c4) + + item['IndividualCodes'] = '\n'.join(individual_codes) show_library = cfg.MODULES.is_enabled('library') return render_template( - 'edit_library.html', + 'item_edit.html', username=session['username'], item=item, library_module_enabled=show_library, @@ -6420,13 +6432,24 @@ def item_edit(id): flash('Element in der Datenbank nicht gefunden.', 'error') return redirect(redirect_target) - # Formulardaten auslesen und bereinigen + # Formulardaten auslesen name = sanitize_form_value(request.form.get('name')) ort = sanitize_form_value(request.form.get('ort')) beschreibung = sanitize_form_value(request.form.get('beschreibung')) - code_4 = sanitize_form_value(request.form.get('code_4')) - isbn_raw = sanitize_form_value(request.form.get('isbn', '')) + # Basis-Code & Einzelcodes + code_4 = sanitize_form_value(request.form.get('code_4')) + individual_codes_raw = request.form.get('individual_codes', '') + + individual_codes = [] + for c in individual_codes_raw.replace('\r', '').split('\n'): + clean_c = sanitize_form_value(c) + if clean_c and clean_c != code_4 and clean_c not in individual_codes: + individual_codes.append(clean_c) + + all_codes_to_check = [code_4] + individual_codes + + isbn_raw = sanitize_form_value(request.form.get('isbn', '')) anschaffungs_jahr = sanitize_form_value(request.form.get('anschaffungsjahr')) anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten')) reservierbar = 'reservierbar' in request.form @@ -6438,15 +6461,32 @@ def item_edit(id): filter2 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter2')), 2) filter3 = sanitize_form_value(request.form.getlist('filter3')) - # WICHTIG: Den aktuellen Verfügbarkeitsstatus aus der Datenbank übernehmen verfuegbar = current_item.get('Verfuegbar', True) - # Barcode Prüfen - if code_4 and not it.is_code_unique(code_4, exclude_id=str(id)): - flash(f'Der Code "{code_4}" wird bereits verwendet.', 'error') + # Sicherer Uniqueness-Check für alle Codes über eine lokale DB-Verbindung + current_group_id = current_item.get('SeriesGroupId') + client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) + db_instance = client[cfg.MONGODB_DB] + items_col = db_instance['items'] + + has_code_error = False + for code in all_codes_to_check: + if not code: + continue + existing = items_col.find_one({'Code_4': code, 'Deleted': {'$ne': True}}) + if existing: + is_same_item = str(existing['_id']) == str(id) + is_in_same_group = current_group_id and existing.get('SeriesGroupId') == current_group_id + if not is_same_item and not is_in_same_group: + flash(f'Der Code "{code}" wird bereits von einem anderen Artikel verwendet.', 'error') + has_code_error = True + break + client.close() + + if has_code_error: return redirect(redirect_target) - # ISBN und Medientyp verarbeiten + # ISBN validieren item_isbn = '' item_type = item_type_input or current_item.get('ItemType', 'general') if cfg.MODULES.is_enabled('library') and isbn_raw: @@ -6455,7 +6495,7 @@ def item_edit(id): flash('Ungültiges ISBN-Format.', 'error') return redirect(redirect_target) - # Bilder verarbeiten (GridFS) + # Bilder verarbeiten images_to_keep = request.form.getlist('existing_images') original_images = current_item.get('Images', []) images = [img for img in original_images if img in images_to_keep] @@ -6469,30 +6509,23 @@ def item_edit(id): if not is_allowed: flash(error_msg, 'error') return redirect(redirect_target) - try: secure_name = secure_filename(file.filename) file.seek(0) image_bytes = file.read() - if not image_bytes: continue - optimized_io = io.BytesIO() with Image.open(io.BytesIO(image_bytes)) as img: if img.mode not in ('RGB', 'RGBA'): img = img.convert('RGBA') - max_width = 800 if img.width > max_width: ratio = max_width / img.width img = img.resize((max_width, int(img.height * ratio)), Image.Resampling.LANCZOS) - img.save(optimized_io, format='WEBP', quality=85, optimize=True) - optimized_io.seek(0) new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp" - fs.put( optimized_io, filename=new_filename, @@ -6506,6 +6539,10 @@ def item_edit(id): if ort and ort not in it.get_predefined_locations(): it.add_predefined_location(ort) + # 1. Gruppen-Bestand über die neue Helfer-Funktion synchronisieren + it.sync_group_codes(str(id), code_4, individual_codes) + + # 2. Deine zentrale update_item Funktion aufrufen success = it.update_item( id=str(id), name=name, diff --git a/Web/modules/database/items.py b/Web/modules/database/items.py index f2c188a..b5b69b8 100755 --- a/Web/modules/database/items.py +++ b/Web/modules/database/items.py @@ -20,6 +20,7 @@ Collection Structure: """ from bson.objectid import ObjectId from bson.errors import InvalidId +import uuid import datetime import Web.modules.database.settings as cfg from Web.modules.database.settings import MongoClient @@ -1147,4 +1148,77 @@ def get_current_status(item_id, decrypt=True): return None except Exception as e: print(f"Error retrieving current status for item {item_id}: {e}") - return None \ No newline at end of file + return None + +def sync_group_codes(primary_obj_id, base_code, individual_codes_list): + """ + Synchronisiert die Barcodes einer Gruppe beim Bearbeiten (abwärtskompatibel). + Trennt sauber zwischen 'Basis-Code' und 'weiteren Einzelcodes'. + """ + if not base_code: + return False + + # Alle Codes zusammenführen und Duplikate filtern + all_target_codes = [base_code] + for c in individual_codes_list: + if c and c not in all_target_codes: + all_target_codes.append(c) + + try: + client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) + db = client[cfg.MONGODB_DB] + items = db['items'] + + primary_item = items.find_one({'_id': ObjectId(primary_obj_id)}) + if not primary_item: + client.close() + return False + + group_id = primary_item.get('SeriesGroupId') + + # 1. Wenn mehrere Codes existieren, aber noch keine Gruppe: Gruppe erstellen + if not group_id and len(all_target_codes) > 1: + group_id = str(uuid.uuid4()) + items.update_one({'_id': primary_item['_id']}, {'$set': {'SeriesGroupId': group_id}}) + primary_item['SeriesGroupId'] = group_id + + # 2. Wenn es weiterhin keine Gruppe gibt (nur 1 Code), updaten und beenden + if not group_id: + items.update_one({'_id': primary_item['_id']}, {'$set': {'Code_4': base_code}}) + client.close() + return True + + # 3. Bestand abgleichen + existing_items = list(items.find({'SeriesGroupId': group_id})) + existing_codes_map = {item.get('Code_4'): item['_id'] for item in existing_items if item.get('Code_4')} + existing_codes_set = set(existing_codes_map.keys()) + target_codes_set = set(all_target_codes) + + codes_to_add = target_codes_set - existing_codes_set + codes_to_remove = existing_codes_set - target_codes_set + + # 4. Entfernte Codes löschen (Haupt-Item wird niemals gelöscht) + for code in codes_to_remove: + item_id_to_delete = existing_codes_map.get(code) + if item_id_to_delete and item_id_to_delete != primary_item['_id']: + items.delete_one({'_id': item_id_to_delete}) + + # 5. Neue Items für neu eingetragene Codes als Klone erstellen + for code in codes_to_add: + new_item = primary_item.copy() + if '_id' in new_item: + del new_item['_id'] + new_item['Code_4'] = code + new_item['is_grouped_sub_item'] = True + new_item['parent_item_id'] = str(primary_item['_id']) + items.insert_one(new_item) + + # 6. Sicherstellen, dass das Haupt-Item den Basis-Code behält + if primary_item.get('Code_4') != base_code: + items.update_one({'_id': primary_item['_id']}, {'$set': {'Code_4': base_code}}) + + client.close() + return True + except Exception as e: + print(f"Error syncing group codes: {e}") + return False \ No newline at end of file diff --git a/Web/templates/edit_library.html b/Web/templates/edit_library.html index 16dac25..2e20b48 100644 --- a/Web/templates/edit_library.html +++ b/Web/templates/edit_library.html @@ -763,17 +763,24 @@ - +
- +
- +
+ +
+ + + Der Scanner füllt zuerst den Basis-Code; weitere gescannte Codes werden hier automatisch zeilenweise angehängt. +
+ {% if show_library_features %}
@@ -917,8 +924,10 @@ function startCode4Scanner() { const scannerBox = document.getElementById('code4-scanner'); const scanButton = document.getElementById('scan-code4-btn'); - const codeField = document.getElementById('code_4'); - if (!scannerBox || !scanButton || !codeField) return; + const baseCodeField = document.getElementById('code_4'); + const individualCodesField = document.getElementById('individual_codes'); + + if (!scannerBox || !scanButton || !baseCodeField) return; if (scannerBox.style.display !== 'none') { killScannerHardware(); scannerBox.style.display = 'none'; scanButton.textContent = 'Barcode scannen'; return; @@ -931,11 +940,25 @@ code4LastScanned = decodedText; code4LastScannedAt = now; killScannerHardware(); scannerBox.style.display = 'none'; scanButton.textContent = 'Barcode scannen'; - codeField.value = decodedText; - validateCodeField(codeField, getItemId()); + + // Intelligentes Eintragen: Entweder Basis-Code oder Textarea + if (!baseCodeField.value.trim()) { + baseCodeField.value = decodedText; + } else { + let currentCodes = individualCodesField.value.split('\n').map(c => c.trim()).filter(c => c); + if (!currentCodes.includes(decodedText) && baseCodeField.value.trim() !== decodedText) { + currentCodes.push(decodedText); + individualCodesField.value = currentCodes.join('\n'); + } + } }, 'Scanner läuft...', (msg) => { document.getElementById('code4-scan-status').textContent = msg; }); } + document.addEventListener('DOMContentLoaded', function() { + const scanCodeBtn = document.getElementById('scan-code4-btn'); + if (scanCodeBtn) scanCodeBtn.addEventListener('click', startCode4Scanner); + }); + function validateCodeField(input, excludeId) { const code = input.value.trim(); if (!code) return;