fix of the json upload format

This commit is contained in:
2026-08-04 21:42:52 +02:00
parent f0b5edff79
commit bc274da006
+45 -45
View File
@@ -11863,6 +11863,23 @@ def batch_upload_page():
return render_template('upload_batch.html') return render_template('upload_batch.html')
def clean_db_field(val):
"""Bereinigt Werte, die fälschlicherweise als String-Listen aus der CSV kommen."""
import pandas as pd
if not val or pd.isna(val):
return None
val_str = str(val).strip()
# Erkennt und entpackt String-Listen wie "['100465']" oder '["100465"]'
if (val_str.startswith("['") and val_str.endswith("']")) or (val_str.startswith('["') and val_str.endswith('"]')):
inner = val_str[2:-2].strip()
return inner if inner and inner != "''" and inner != '""' else None
if val_str in ("[]", "['']", '[""]', "nan", "None"):
return None
return val_str
@app.route('/upload_csv_batch', methods=['POST']) @app.route('/upload_csv_batch', methods=['POST'])
def upload_csv_batch(): def upload_csv_batch():
@@ -11878,13 +11895,6 @@ def upload_csv_batch():
username = session.get('username', 'System') username = session.get('username', 'System')
def generate_unique_batch_code(base_code, position): def generate_unique_batch_code(base_code, position):
"""
Generiert einen eindeutigen Code für einen Artikel innerhalb einer Serie (Batch).
:param base_code: Der Code des ersten Artikels in der Gruppe (String oder None).
:param position: Die Position des aktuellen Artikels in der Gruppe (Integer).
:return: Ein eindeutiger Code als String.
"""
if base_code: if base_code:
return f"{base_code}-{position}" return f"{base_code}-{position}"
else: else:
@@ -11913,8 +11923,8 @@ def upload_csv_batch():
return jsonify({"success": False, "message": "Die CSV muss zwingend eine 'Name' Spalte enthalten."}), 400 return jsonify({"success": False, "message": "Die CSV muss zwingend eine 'Name' Spalte enthalten."}), 400
# 3. Bilder verarbeiten & Duplikate im selben Durchlauf filtern (Hash-Matching) # 3. Bilder verarbeiten & Duplikate im selben Durchlauf filtern (Hash-Matching)
image_mapping = {} # Original-Dateiname (ohne Ext) -> GridFS Filename (.webp) image_mapping = {}
processed_hashes = {} # SHA-256 Hash -> GridFS Filename (.webp) processed_hashes = {}
processed_count = 0 processed_count = 0
dedup_count = 0 dedup_count = 0
error_count = 0 error_count = 0
@@ -11934,18 +11944,14 @@ def upload_csv_batch():
error_count += 1 error_count += 1
continue continue
# SHA-256 Hash des Bildinhalts zur Erkennung identischer Bilder
img_hash = hashlib.sha256(image_bytes).hexdigest() img_hash = hashlib.sha256(image_bytes).hexdigest()
if img_hash in processed_hashes: if img_hash in processed_hashes:
# Bild ist identisch zu einem bereits verarbeiteten Bild im selben Batch
existing_filename = processed_hashes[img_hash] existing_filename = processed_hashes[img_hash]
image_mapping[base_name_no_ext] = existing_filename image_mapping[base_name_no_ext] = existing_filename
dedup_count += 1 dedup_count += 1
app.logger.info(f"{image_log_prefix} Duplikat erkannt ({original_secure_name}). Wiederverwendung von: {existing_filename}")
continue continue
# Neues Bild verarbeiten und nach WebP konvertieren
optimized_io = io.BytesIO() optimized_io = io.BytesIO()
with Image.open(io.BytesIO(image_bytes)) as img: with Image.open(io.BytesIO(image_bytes)) as img:
if img.mode not in ('RGB', 'RGBA'): if img.mode not in ('RGB', 'RGBA'):
@@ -11962,8 +11968,7 @@ def upload_csv_batch():
optimized_io.seek(0) optimized_io.seek(0)
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp" new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
# In GridFS speichern fs.put(
file_id = fs.put(
optimized_io, optimized_io,
filename=new_filename, filename=new_filename,
content_type='image/webp', content_type='image/webp',
@@ -11974,7 +11979,6 @@ def upload_csv_batch():
} }
) )
# In Hash-Tabelle und Mapping sichern
processed_hashes[img_hash] = new_filename processed_hashes[img_hash] = new_filename
image_mapping[base_name_no_ext] = new_filename image_mapping[base_name_no_ext] = new_filename
processed_count += 1 processed_count += 1
@@ -11989,8 +11993,8 @@ def upload_csv_batch():
except Exception: except Exception:
predefined_locations = [] predefined_locations = []
# 5. Dataframe bereinigen & gruppieren # 5. Dataframe bereinigen
df['Name'] = df['Name'].fillna('Unbenannt').astype(str) df['Name'] = df['Name'].fillna('Unbenannt').astype(str).str.strip()
df = df.fillna({ df = df.fillna({
'Ort': 'Unbekannt', 'Ort': 'Unbekannt',
'Beschreibung': '', 'Beschreibung': '',
@@ -11999,21 +12003,28 @@ def upload_csv_batch():
'Anschaffungskosten': '' 'Anschaffungskosten': ''
}) })
created_item_ids = [] # --- WICHTIG: Gruppierung über einen normalisierten Schlüssel ermöglichen ---
grouped_items = df.groupby('Name') # Erstellt eine unsichtbare Hilfsspalte, die Leerzeichen/Groß-Kleinschreibung ignoriert,
# damit identische Artikel-Typen sauber als Serie erkannt werden.
df['GroupKey'] = df['Name'].str.lower()
for name, group in grouped_items: created_item_ids = []
grouped_items = df.groupby('GroupKey')
for group_key, group in grouped_items:
item_count = len(group) item_count = len(group)
series_group_id = str(uuid.uuid4()) if item_count > 1 else None series_group_id = str(uuid.uuid4()) if item_count > 1 else None
parent_item_id = None parent_item_id = None
# Originalen Namen des ersten Elements der Gruppe übernehmen
actual_group_name = group.iloc[0]['Name']
# Basis-Code für automatisierte Seriencodes ermitteln # Basis-Code für automatisierte Seriencodes ermitteln
first_row_code = str(group.iloc[0].get('Code_4', '')).strip() first_row_code = clean_db_field(group.iloc[0].get('Code_4', ''))
base_code = first_row_code if first_row_code else None base_code = first_row_code if first_row_code else None
for position, (index, row) in enumerate(group.iterrows(), start=1): for position, (index, row) in enumerate(group.iterrows(), start=1):
# Ort automatisch zu predefined_locations hinzufügen, falls neu
ort_val = str(row['Ort']).strip() ort_val = str(row['Ort']).strip()
if ort_val and ort_val not in predefined_locations: if ort_val and ort_val not in predefined_locations:
try: try:
@@ -12022,7 +12033,7 @@ def upload_csv_batch():
except Exception as e: except Exception as e:
app.logger.warning(f"Ort {ort_val} konnte nicht hinzugefügt werden: {e}") app.logger.warning(f"Ort {ort_val} konnte nicht hinzugefügt werden: {e}")
# Bilder für diesen Artikel zuordnen # Bilder zuordnen und pro Artikel deduplizieren
item_image_filenames = [] item_image_filenames = []
if 'Images' in row and pd.notna(row['Images']): if 'Images' in row and pd.notna(row['Images']):
try: try:
@@ -12032,19 +12043,13 @@ def upload_csv_batch():
base_img_name = os.path.splitext(img_name)[0] base_img_name = os.path.splitext(img_name)[0]
if base_img_name in image_mapping: if base_img_name in image_mapping:
item_image_filenames.append(image_mapping[base_img_name]) item_image_filenames.append(image_mapping[base_img_name])
else:
app.logger.warning(f"Bild {img_name} in CSV definiert, aber nicht hochgeladen.")
except (ValueError, SyntaxError): except (ValueError, SyntaxError):
pass pass
# --- NEU: BILDER-REFERENZEN PRO ARTIKEL DEDUPLIZIEREN ---
# Falls die CSV z.B. ['bild1.jpg', 'bild1.jpg'] enthält, filtern wir das hier heraus,
# damit die GridFS-Datei nicht doppelt als Referenz gespeichert wird.
unique_image_filenames = [] unique_image_filenames = []
for img in item_image_filenames: for img in item_image_filenames:
if img not in unique_image_filenames: if img not in unique_image_filenames:
unique_image_filenames.append(img) unique_image_filenames.append(img)
# --------------------------------------------------------
def parse_filter_col(col_data): def parse_filter_col(col_data):
try: try:
@@ -12059,26 +12064,26 @@ def upload_csv_batch():
reservierbar = bool(row.get('Reservierbar', False)) reservierbar = bool(row.get('Reservierbar', False))
# Code_4 Behandlung: Falls in CSV definiert nutzen, sonst Batch-Code erzeugen # Code_4 Behandlung
row_code = str(row.get('Code_4', '')).strip() row_code = clean_db_field(row.get('Code_4', ''))
if row_code: if row_code:
unique_code = row_code unique_code = row_code
elif 'generate_unique_batch_code' in globals(): elif item_count > 1:
unique_code = generate_unique_batch_code(base_code, position) unique_code = generate_unique_batch_code(base_code, position)
else: else:
unique_code = None unique_code = None
# DB Insert (exakt abgestimmt auf die 10 positionellen Argumente) # DB Insert
item_id = it.add_item( item_id = it.add_item(
str(row['Name']), # 1. Name str(actual_group_name), # 1. Name
ort_val, # 2. Ort ort_val, # 2. Ort
str(row['Beschreibung']), # 3. Beschreibung str(row['Beschreibung']), # 3. Beschreibung
unique_image_filenames, # 4. Image Filenames (GridFS) -> HIER GEÄNDERT unique_image_filenames, # 4. Image Filenames (GridFS)
filter_upload, # 5. Filter 1 filter_upload, # 5. Filter 1
filter_upload2, # 6. Filter 2 filter_upload2, # 6. Filter 2
filter_upload3, # 7. Filter 3 filter_upload3, # 7. Filter 3
str(row['Anschaffungsjahr']) if row['Anschaffungsjahr'] else None, # 8. Jahr clean_db_field(row.get('Anschaffungsjahr')), # 8. Jahr
str(row['Anschaffungskosten']) if row['Anschaffungskosten'] else None, # 9. Kosten clean_db_field(row.get('Anschaffungskosten')),# 9. Kosten
unique_code, # 10. Unique Code / Code_4 unique_code, # 10. Unique Code / Code_4
reservierbar=reservierbar, reservierbar=reservierbar,
series_group_id=series_group_id, series_group_id=series_group_id,
@@ -12097,16 +12102,11 @@ def upload_csv_batch():
if position == 1: if position == 1:
parent_item_id = str(item_id) parent_item_id = str(item_id)
else: else:
app.logger.error(f"Fehler beim Erstellen von Item: {row['Name']} (Index {index})") app.logger.error(f"Fehler beim Erstellen von Item: {actual_group_name} (Index {index})")
app.logger.info(
f"Batch Upload abgeschlossen: {len(created_item_ids)} Items erstellt. "
f"{processed_count} neue Bilder hochgeladen, {dedup_count} Bild-Duplikate zusammengeführt."
)
return jsonify({ return jsonify({
"success": True, "success": True,
"message": f"Upload erfolgreich. {len(created_item_ids)} Items importiert. {processed_count} neue Bilder gespeichert ({dedup_count} Duplikate zusammengeführt).", "message": f"Upload erfolgreich. {len(created_item_ids)} Items importiert.",
"created_count": len(created_item_ids), "created_count": len(created_item_ids),
"images_processed": processed_count, "images_processed": processed_count,
"images_deduplicated": dedup_count, "images_deduplicated": dedup_count,