Compare commits

..

9 Commits

Author SHA1 Message Date
Aiirondev_dev 6a3865ef24 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-12 11:46:59 +02:00
Aiirondev_dev 96d45710ac fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-12 11:45:48 +02:00
Aiirondev_dev dd3d8649a7 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 3m10s
2026-08-12 11:37:05 +02:00
Aiirondev_dev 1af2a2be06 implementation of the image
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 00:32:00 +02:00
Aiirondev_dev 8e5e434116 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-12 00:24:25 +02:00
Aiirondev_dev 2f9a93ee65 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 00:20:48 +02:00
Aiirondev_dev 91467a1e76 fix of the template name
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-12 00:19:19 +02:00
Aiirondev_dev 3840348a2d Changes to the formating and processing of the group codes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-12 00:12:06 +02:00
Aiirondev_dev cdb7319c56 Changes so the edeting function can also adeit all the Codes and the entire mamunt of an itemgroup
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 23:58:23 +02:00
4 changed files with 1487 additions and 894 deletions
+114 -94
View File
@@ -6366,11 +6366,25 @@ def edit_item(id):
return redirect(url_for('home_admin'))
def is_library_item(item):
"""
Prüft, ob ein Artikel ein Bibliotheks-Item ist.
- 'other', None oder Leerstring -> Inventarsystem (False)
- Jeder andere Medientyp ('Buch', 'CD', etc.) -> Bibliothek (True)
"""
if not item:
return False
item_type = item.get('ItemType', 'other')
if not item_type:
return False
return item_type.strip().lower() != 'other'
@app.route('/item_edit/<id>', methods=['GET', 'POST'])
def item_edit(id):
"""
Endpoint zum Laden und Aktualisieren eines Eintrags (item_edit).
Complete endpoint for editing items. Automatically detects whether the item
is a Library item (ItemType != 'other') or an Inventory item (ItemType == 'other').
"""
if 'username' not in session:
if request.method == 'POST' and request.is_json:
@@ -6391,20 +6405,27 @@ def item_edit(id):
flash('Ungültige Element-ID.', 'error')
return redirect(url_for('home_admin'))
# --- GET: Template anzeigen ---
current_item = it.get_item(obj_id)
if not current_item:
flash('Element in der Datenbank nicht gefunden.', 'error')
return redirect(url_for('home_admin'))
# Determine item type classification
library_module_active = cfg.MODULES.is_enabled('library')
is_lib_item = it.is_library_item(current_item)
show_library_features = library_module_active and is_lib_item
# -------------------------------------------------------------------
# GET METHOD: Render Form
# -------------------------------------------------------------------
if request.method == 'GET':
item = it.get_item(id)
if not item:
flash('Element nicht gefunden.', 'error')
return redirect(url_for('home_admin'))
current_item['_id'] = str(current_item['_id'])
item['_id'] = str(item['_id'])
# Gruppen-Codes aufteilen in Basis-Code und Einzelcodes für die Textarea
base_code = item.get('Code_4', '')
# Format individual group codes for the textarea
base_code = current_item.get('Code_4', '')
individual_codes = []
if item.get('SeriesGroupId'):
group_ids = it.get_group_item_ids(str(item['_id']))
if current_item.get('SeriesGroupId'):
group_ids = it.get_group_item_ids(str(current_item['_id']))
if group_ids:
for gid in group_ids:
g_item = it.get_item(gid)
@@ -6412,32 +6433,31 @@ def item_edit(id):
if c4 and c4 != base_code:
individual_codes.append(c4)
item['IndividualCodes'] = '\n'.join(individual_codes)
show_library = cfg.MODULES.is_enabled('library')
current_item['IndividualCodes'] = '\n'.join(individual_codes)
return render_template(
'item_edit.html',
'edit_library.html',
username=session['username'],
item=item,
library_module_enabled=show_library,
show_library_features=show_library,
page_title=f"Bearbeiten: {item.get('Name', '')}"
item=current_item,
show_library_features=show_library_features,
library_module_enabled=library_module_active,
page_title=f"Bearbeiten: {current_item.get('Name', '')}"
)
# --- POST: Speichern ---
# -------------------------------------------------------------------
# POST METHOD: Save Changes
# -------------------------------------------------------------------
redirect_target = request.referrer or url_for('home_admin')
current_item = it.get_item(obj_id)
if not current_item:
flash('Element in der Datenbank nicht gefunden.', 'error')
return redirect(redirect_target)
# Formulardaten auslesen
# Common fields
name = sanitize_form_value(request.form.get('name'))
ort = sanitize_form_value(request.form.get('ort'))
beschreibung = sanitize_form_value(request.form.get('beschreibung'))
anschaffungs_jahr = sanitize_form_value(request.form.get('anschaffungsjahr'))
anschaffungs_kosten = sanitize_form_value(request.form.get('anschaffungskosten'))
reservierbar = 'reservierbar' in request.form
# Basis-Code & Einzelcodes
# Barcodes
code_4 = sanitize_form_value(request.form.get('code_4'))
individual_codes_raw = request.form.get('individual_codes', '')
@@ -6449,21 +6469,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
# Barcode uniqueness check
current_group_id = current_item.get('SeriesGroupId')
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
db_instance = client[cfg.MONGODB_DB]
@@ -6486,70 +6492,84 @@ 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)
# Type-specific processing
if show_library_features:
# --- LIBRARY ITEM ---
isbn_raw = sanitize_form_value(request.form.get('isbn', ''))
item_isbn = normalize_and_validate_isbn(isbn_raw) if isbn_raw else ''
item_type = sanitize_form_value(request.form.get('item_type_input', 'Buch'))
library_category = sanitize_form_value(request.form.get('library_category', ''))
# 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]
filter1 = current_item.get('Filter', [])
filter2 = current_item.get('Filter2', [])
filter3 = current_item.get('Filter3', [])
images = current_item.get('Images', [])
else:
# --- INVENTORY ITEM ---
item_isbn = current_item.get('ISBN', '')
item_type = 'other' # Standard type for inventory items
library_category = current_item.get('library_category', '')
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}")
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'))
# Manage images for Inventory Mode
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"Image error for item {id}: {e}")
# Auto-add new location to predefined locations list if applicable
if ort and ort not in it.get_predefined_locations():
it.add_predefined_location(ort)
# 1. Gruppen-Bestand über die neue Helfer-Funktion synchronisieren
# Sync series/group barcode IDs
it.sync_group_codes(str(id), code_4, individual_codes)
# 2. Deine zentrale update_item Funktion aufrufen
# Update item in database
success = it.update_item(
id=str(id),
name=name,
ort=ort,
beschreibung=beschreibung,
images=images,
verfuegbar=verfuegbar,
verfuegbar=current_item.get('Verfuegbar', True),
filter1=filter1,
filter2=filter2,
filter3=filter3,
@@ -6558,7 +6578,8 @@ def item_edit(id):
code_4=code_4,
reservierbar=reservierbar,
isbn=item_isbn,
item_type=item_type
item_type=item_type,
library_category=library_category
)
if success:
@@ -6567,7 +6588,6 @@ def item_edit(id):
flash('Fehler beim Aktualisieren des Artikels.', 'error')
return redirect(redirect_target)
@app.route('/update_group', methods=['POST'])
def update_group():
+94 -36
View File
@@ -27,6 +27,19 @@ from Web.modules.database.settings import MongoClient
import Web.modules.inventarsystem.data_protection as dp
def is_library_item(item):
"""
Determines if an item belongs to the library system.
Returns False for 'other', None, or empty ItemType (Inventory item).
Returns True for any specific library type ('Buch', 'CD', 'DVD', etc.).
"""
if not item:
return False
item_type = item.get('ItemType', 'other')
if not item_type:
return False
return str(item_type).strip().lower() != 'other'
def safe_decrypt_user(encrypted_user):
"""
Safely decrypt an encrypted username string.
@@ -1152,18 +1165,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 +1191,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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff