diff --git a/Web/app.py b/Web/app.py index 135c6d2..7b39370 100755 --- a/Web/app.py +++ b/Web/app.py @@ -11874,18 +11874,14 @@ csrf = CSRFProtect(app) def upload_csv_batch(): """ Route for batch adding new items to the inventory via CSV. - Handles CSV parsing, bulk image upload (conversion to WebP), GridFS storage, - and groups identical items based on their Name. + Handles CSV parsing, bulk image upload with deduplication (SHA-256 hash matching), + GridFS storage, code generation, location syncing, and grouped item creation. """ import pandas as pd import ast - #if 'username' not in session: - # return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401 + import hashlib - username = session['username'] - # permissions = _get_current_user_permissions() ... (anpassen wie in Original) - # if not _action_access_allowed(permissions, 'can_insert'): - # return jsonify({'success': False, 'message': 'Einfüge-Rechte erforderlich'}), 403 + username = session.get('username', 'System') fs = get_gridfs() upload_session_id = str(uuid.uuid4())[:8] @@ -11908,10 +11904,11 @@ def upload_csv_batch(): if 'Name' not in df.columns: return jsonify({"success": False, "message": "Die CSV muss zwingend eine 'Name' Spalte enthalten."}), 400 - # 3. Bilder verarbeiten, nach WebP konvertieren und in GridFS speichern - # Mapping: Original-Dateiname (ohne Pfad/Erweiterung) -> GridFS Filename (.webp) - image_mapping = {} + # 3. Bilder verarbeiten & Duplikate im selben Durchlauf filtern (Hash-Matching) + image_mapping = {} # Original-Dateiname (ohne Ext) -> GridFS Filename (.webp) + processed_hashes = {} # SHA-256 Hash -> GridFS Filename (.webp) processed_count = 0 + dedup_count = 0 error_count = 0 for index, image in enumerate(uploaded_images): @@ -11929,6 +11926,18 @@ def upload_csv_batch(): error_count += 1 continue + # SHA-256 Hash des Bildinhalts zur Erkennung identischer Bilder + img_hash = hashlib.sha256(image_bytes).hexdigest() + + if img_hash in processed_hashes: + # Bild ist identisch zu einem bereits verarbeiteten Bild im selben Batch + existing_filename = processed_hashes[img_hash] + image_mapping[base_name_no_ext] = existing_filename + dedup_count += 1 + app.logger.info(f"{image_log_prefix} Duplikat erkannt ({original_secure_name}). Wiederverwendung von: {existing_filename}") + continue + + # Neues Bild verarbeiten und nach WebP konvertieren optimized_io = io.BytesIO() with Image.open(io.BytesIO(image_bytes)) as img: if img.mode not in ('RGB', 'RGBA'): @@ -11945,7 +11954,7 @@ def upload_csv_batch(): optimized_io.seek(0) new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp" - # Speichern in GridFS analog zu upload_item + # In GridFS speichern file_id = fs.put( optimized_io, filename=new_filename, @@ -11957,7 +11966,8 @@ def upload_csv_batch(): } ) - # Im Mapping speichern (damit wir sie später der CSV zuordnen können) + # In Hash-Tabelle und Mapping sichern + processed_hashes[img_hash] = new_filename image_mapping[base_name_no_ext] = new_filename processed_count += 1 @@ -11965,10 +11975,14 @@ def upload_csv_batch(): app.logger.error(f"{image_log_prefix} Processing failed: {str(e)}") error_count += 1 - # 4. Items gruppieren (Analog zu series_group_id aus upload_item) - df['Name'] = df['Name'].fillna('Unbenannt').astype(str) + # 4. Predefined Locations laden + try: + predefined_locations = it.get_predefined_locations() + except Exception: + predefined_locations = [] - # Optional: Fülle NaN Werte in der CSV mit sinnvollen Defaults für die Datenbank + # 5. Dataframe bereinigen & gruppieren + df['Name'] = df['Name'].fillna('Unbenannt').astype(str) df = df.fillna({ 'Ort': 'Unbekannt', 'Beschreibung': '', @@ -11985,9 +11999,22 @@ def upload_csv_batch(): series_group_id = str(uuid.uuid4()) if item_count > 1 else None parent_item_id = None + # Basis-Code für automatisierte Seriencodes ermitteln + first_row_code = str(group.iloc[0].get('Code_4', '')).strip() + base_code = first_row_code if first_row_code else None + for position, (index, row) in enumerate(group.iterrows(), start=1): - # Bilder aus der CSV-Zeile extrahieren und über das image_mapping mappen + # Ort automatisch zu predefined_locations hinzufügen, falls neu + ort_val = str(row['Ort']).strip() + if ort_val and ort_val not in predefined_locations: + try: + it.add_predefined_location(ort_val) + predefined_locations.append(ort_val) + except Exception as e: + app.logger.warning(f"Ort {ort_val} konnte nicht hinzugefügt werden: {e}") + + # Bilder für diesen Artikel zuordnen item_image_filenames = [] if 'Images' in row and pd.notna(row['Images']): try: @@ -12006,7 +12033,7 @@ def upload_csv_batch(): try: res = ast.literal_eval(str(col_data)) return res if isinstance(res, list) else [] - except: + except Exception: return [] filter_upload = parse_filter_col(row.get('Filter', '[]')) @@ -12015,28 +12042,37 @@ def upload_csv_batch(): reservierbar = bool(row.get('Reservierbar', False)) - # DB Insert Funktion aufrufen (mit korrigierten, positionellen Parametern) + # Code_4 Behandlung: Falls in CSV definiert nutzen, sonst Batch-Code erzeugen + row_code = str(row.get('Code_4', '')).strip() + if row_code: + unique_code = row_code + elif 'generate_unique_batch_code' in globals(): + unique_code = generate_unique_batch_code(base_code, position) + else: + unique_code = None + + # DB Insert (exakt abgestimmt auf die 10 positionellen Argumente) item_id = it.add_item( - str(row['Name']), - str(row['Ort']), - str(row['Beschreibung']), - item_image_filenames, - filter_upload, - filter_upload2, - filter_upload3, - str(row['Anschaffungsjahr']) if row['Anschaffungsjahr'] else None, - str(row['Anschaffungskosten']) if row['Anschaffungskosten'] else None, - str(row['Code_4']) if row['Code_4'] else None, + str(row['Name']), # 1. Name + ort_val, # 2. Ort + str(row['Beschreibung']), # 3. Beschreibung + item_image_filenames, # 4. Image Filenames (GridFS) + filter_upload, # 5. Filter 1 + filter_upload2, # 6. Filter 2 + filter_upload3, # 7. Filter 3 + str(row['Anschaffungsjahr']) if row['Anschaffungsjahr'] else None, # 8. Jahr + str(row['Anschaffungskosten']) if row['Anschaffungskosten'] else None, # 9. Kosten + unique_code, # 10. Unique Code / Code_4 reservierbar=reservierbar, series_group_id=series_group_id, series_count=item_count, series_position=position, is_grouped_sub_item=(position > 1), parent_item_id=parent_item_id, - isbn='', - item_type='other', - library_category='', - is_library=False + isbn=str(row.get('ISBN', '')), + item_type=str(row.get('Item_Type', 'other')), + library_category=str(row.get('Library_Category', '')), + is_library=bool(row.get('Is_Library', False)) ) if item_id: @@ -12047,12 +12083,15 @@ def upload_csv_batch(): app.logger.error(f"Fehler beim Erstellen von Item: {row['Name']} (Index {index})") app.logger.info( - f"Batch Upload abgeschlossen: {len(created_item_ids)} Items erstellt. {processed_count} Bilder verarbeitet.") + f"Batch Upload abgeschlossen: {len(created_item_ids)} Items erstellt. " + f"{processed_count} neue Bilder hochgeladen, {dedup_count} Bild-Duplikate zusammengeführt." + ) return jsonify({ "success": True, - "message": f"Upload erfolgreich. {len(created_item_ids)} Items importiert und {processed_count} Bilder konvertiert.", + "message": f"Upload erfolgreich. {len(created_item_ids)} Items importiert. {processed_count} neue Bilder gespeichert ({dedup_count} Duplikate zusammengeführt).", "created_count": len(created_item_ids), "images_processed": processed_count, + "images_deduplicated": dedup_count, "images_failed": error_count }), 200 \ No newline at end of file