diff --git a/Web/app.py b/Web/app.py index 5befa10..6a3d90e 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6400,10 +6400,12 @@ def item_edit(id): item['_id'] = str(item['_id']) - # Gruppen-Codes aufteilen in Basis-Code und Einzelcodes für die Textarea + # Codes für die Ansicht trennen (Basis-Code vs. Einzelcodes) base_code = item.get('Code_4', '') individual_codes = [] - if item.get('SeriesGroupId'): + + group_id = item.get('SeriesGroupId') + if group_id: group_ids = it.get_group_item_ids(str(item['_id'])) if group_ids: for gid in group_ids: @@ -6416,7 +6418,7 @@ def item_edit(id): 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, @@ -6424,7 +6426,7 @@ def item_edit(id): page_title=f"Bearbeiten: {item.get('Name', '')}" ) - # --- POST: Speichern --- + # --- POST: Speichern --- redirect_target = request.referrer or url_for('home_admin') current_item = it.get_item(obj_id) @@ -6437,7 +6439,7 @@ def item_edit(id): ort = sanitize_form_value(request.form.get('ort')) beschreibung = sanitize_form_value(request.form.get('beschreibung')) - # Basis-Code & Einzelcodes + # Basis-Code & Einzelcodes aus der Textarea code_4 = sanitize_form_value(request.form.get('code_4')) individual_codes_raw = request.form.get('individual_codes', '') @@ -6449,21 +6451,7 @@ def item_edit(id): 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 - - item_type_input = sanitize_form_value(request.form.get('item_type_input')) - library_category = sanitize_form_value(request.form.get('library_category')) - - filter1 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter')), 1) - filter2 = expand_filter_selection(sanitize_form_value(request.form.getlist('filter2')), 2) - filter3 = sanitize_form_value(request.form.getlist('filter3')) - - verfuegbar = current_item.get('Verfuegbar', True) - - # Sicherer Uniqueness-Check für alle Codes über eine lokale DB-Verbindung + # Uniqueness-Check über alle Codes current_group_id = current_item.get('SeriesGroupId') client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db_instance = client[cfg.MONGODB_DB] @@ -6486,63 +6474,10 @@ def item_edit(id): if has_code_error: return redirect(redirect_target) - # ISBN validieren - item_isbn = '' - item_type = item_type_input or current_item.get('ItemType', 'general') - if cfg.MODULES.is_enabled('library') and isbn_raw: - item_isbn = normalize_and_validate_isbn(isbn_raw) - if not item_isbn: - flash('Ungültiges ISBN-Format.', 'error') - return redirect(redirect_target) - - # 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] - - new_files = request.files.getlist('images') - if new_files and new_files[0].filename != '': - fs = get_gridfs() - for file in new_files: - if file and file.filename: - is_allowed, error_msg = allowed_file(file.filename, file) - 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, - content_type='image/webp', - metadata={'original_filename': secure_name, 'item_id': str(id)} - ) - images.append(new_filename) - except Exception as e: - app.logger.error(f"Bild-Fehler bei Item {id}: {e}") - - if ort and ort not in it.get_predefined_locations(): - it.add_predefined_location(ort) - - # 1. Gruppen-Bestand über die neue Helfer-Funktion synchronisieren + # 1. Gruppen-Bestand über die korrigierte Helfer-Funktion synchronisieren it.sync_group_codes(str(id), code_4, individual_codes) - # 2. Deine zentrale update_item Funktion aufrufen + # 2. Deine bestehende 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 b5b69b8..938d421 100755 --- a/Web/modules/database/items.py +++ b/Web/modules/database/items.py @@ -1152,18 +1152,20 @@ def get_current_status(item_id, decrypt=True): 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'. + Synchronisiert die Barcodes einer Gruppe im korrekten Schema + (angelehnt an das 'Augenmodell groß'-Vorbild). """ if not base_code: return False - # Alle Codes zusammenführen und Duplikate filtern + # Alle Ziel-Codes zusammenführen (Basis-Code an erster Stelle) 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) + item_count = len(all_target_codes) + try: client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) db = client[cfg.MONGODB_DB] @@ -1176,46 +1178,89 @@ def sync_group_codes(primary_obj_id, base_code, individual_codes_list): 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: + # Wenn es mehr als 1 Item gibt und noch keine Gruppe existiert -> Neue GroupID erzeugen + if not group_id and item_count > 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 + # Wenn es nun eine Gruppe gibt (item_count > 1) + if item_count > 1: + # 1. Haupt-Item (Parent) aktualisieren + items.update_one( + {'_id': primary_item['_id']}, + {'$set': { + 'Code_4': base_code, + 'SeriesGroupId': group_id, + 'SeriesCount': item_count, + 'SeriesPosition': 1, + 'IsGroupedSubItem': False, + 'ParentItemId': None + }} + ) - # 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) + # Bestehende Gruppenmitglieder laden + existing_items = list(items.find({'SeriesGroupId': group_id})) + existing_map = {it.get('Code_4'): it for it in existing_items if + it.get('Code_4') and str(it['_id']) != str(primary_item['_id'])} - codes_to_add = target_codes_set - existing_codes_set - codes_to_remove = existing_codes_set - target_codes_set + # Alle verbleibenden Sub-Codes ab Position 2 abarbeiten + processed_sub_ids = [] + for idx, code in enumerate(all_target_codes[1:], start=2): + if code in existing_map: + # Existiert bereits in der Gruppe -> Nur Position und Count aktualisieren + sub_item = existing_map[code] + processed_sub_ids.append(sub_item['_id']) + items.update_one( + {'_id': sub_item['_id']}, + {'$set': { + 'SeriesCount': item_count, + 'SeriesPosition': idx, + 'IsGroupedSubItem': True, + 'ParentItemId': str(primary_item['_id']) + }} + ) + else: + # Neu hinzukommender Code -> Als Klon (Sub-Item) erstellen + new_sub = primary_item.copy() + if '_id' in new_sub: + del new_sub['_id'] - # 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}) + new_sub.update({ + 'Code_4': code, + 'SeriesGroupId': group_id, + 'SeriesCount': item_count, + 'SeriesPosition': idx, + 'IsGroupedSubItem': True, + 'ParentItemId': str(primary_item['_id']), + 'LastUpdated': primary_item.get('LastUpdated') + }) + inserted_res = items.insert_one(new_sub) + processed_sub_ids.append(inserted_res.inserted_id) - # 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) + # Nicht mehr benötigte Sub-Items aus dieser Gruppe entfernen + for code, sub_item in existing_map.items(): + if sub_item['_id'] not in processed_sub_ids: + items.delete_one({'_id': sub_item['_id']}) - # 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}}) + else: + # Fall: Nur 1 einziges Item (keine Gruppe / Gruppe aufgelöst) + # Eventuelle alte Sub-Items dieser Gruppe löschen + if group_id: + items.delete_many({ + 'SeriesGroupId': group_id, + '_id': {'$ne': primary_item['_id']} + }) + + items.update_one( + {'_id': primary_item['_id']}, + {'$set': { + 'Code_4': base_code, + 'SeriesGroupId': None, + 'SeriesCount': 1, + 'SeriesPosition': 1, + 'IsGroupedSubItem': False, + 'ParentItemId': None + }} + ) client.close() return True