Additiopn to filter out the iduvidual items and
Release Inventarsystem / release-docker (push) Has been cancelled

This commit is contained in:
2026-08-11 21:16:37 +02:00
parent c90cef6dcf
commit 6a94d50d28
3 changed files with 1185 additions and 193 deletions
+177 -1
View File
@@ -46,7 +46,7 @@ import Web.modules.inventarsystem.pdf_export as pdf_export
import Web.modules.inventarsystem.excel_export as excel_export import Web.modules.inventarsystem.excel_export as excel_export
import datetime import datetime
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from bson.objectid import ObjectId from bson.objectid import ObjectId, InvalidId
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
import requests import requests
import csv import csv
@@ -3314,6 +3314,7 @@ def api_library_items():
query = { query = {
'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']}, 'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']},
'IsGroupedSubItem': {'$ne': True},
'Deleted': {'$ne': True} 'Deleted': {'$ne': True}
} }
@@ -6366,6 +6367,181 @@ def edit_item(id):
return redirect(url_for('home_admin')) return redirect(url_for('home_admin'))
@app.route('/item_edit/<id>', methods=['GET', 'POST'])
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
flash('Bitte melden Sie sich an.', 'error')
return redirect(url_for('login'))
current_permissions = us.get_effective_permissions(session['username'])
if not current_permissions['actions'].get('can_edit', False):
if request.method == 'POST' and request.is_json:
return jsonify({'success': False, 'message': 'Keine Berechtigung zum Bearbeiten.'}), 403
flash('Keine Berechtigung zum Bearbeiten.', 'error')
return redirect(url_for('home_admin'))
try:
obj_id = ObjectId(id)
except InvalidId:
flash('Ungültige Element-ID.', 'error')
return redirect(url_for('home_admin'))
db = it.db
items_col = db['items']
# --- GET: Template anzeigen ---
if request.method == 'GET':
item = items_col.find_one({'_id': obj_id})
if not item:
flash('Element nicht gefunden.', 'error')
return redirect(url_for('home_admin'))
item['_id'] = str(item['_id'])
show_library = cfg.MODULES.is_enabled('library')
return render_template(
'item_edit.html',
username=session['username'],
item=item,
library_module_enabled=show_library,
show_library_features=show_library,
page_title=f"Bearbeiten: {item.get('Name', '')}"
)
# --- POST: Speichern ---
redirect_target = request.referrer or url_for('home_admin')
current_item = items_col.find_one({'_id': obj_id})
if not current_item:
flash('Element in der Datenbank nicht gefunden.', 'error')
return redirect(redirect_target)
# Formulardaten
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', ''))
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'))
# 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')
return redirect(redirect_target)
# ISBN
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)
# Datenstruktur für Update
shared_fields = {
'Name': name,
'Ort': ort,
'Beschreibung': beschreibung,
'Anschaffungsjahr': anschaffungs_jahr,
'Anschaffungskosten': anschaffungs_kosten,
'Reservierbar': reservierbar,
'ISBN': item_isbn,
'ItemType': item_type,
'Kategorie': library_category,
'LastUpdated': datetime.datetime.now()
}
# Gruppen-Update
group_item_ids = it.get_group_item_ids(str(id))
if group_item_ids:
group_object_ids = [ObjectId(g_id) for g_id in group_item_ids]
items_col.update_many({'_id': {'$in': group_object_ids}}, {'$set': shared_fields})
# Individual-Update
individual_update = {
**shared_fields,
'Code_4': code_4,
'Images': images,
'Filter1': filter1,
'Filter2': filter2,
'Filter3': filter3
}
items_col.update_one({'_id': obj_id}, {'$set': individual_update})
flash('Artikel erfolgreich aktualisiert.', 'success')
return redirect(redirect_target)
@app.route('/update_group', methods=['POST']) @app.route('/update_group', methods=['POST'])
def update_group(): def update_group():
File diff suppressed because it is too large Load Diff
+3 -192
View File
@@ -800,7 +800,7 @@
return ` return `
<tr> <tr>
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td> <td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
<td>${escapeHtml(item.Code_4 || item.Code4 || '-')}</td> <td>${escapeHtml(Item.ISBN || '-')}</td>
<td>${getItemTypeLabel(item.ItemType || 'book')}</td> <td>${getItemTypeLabel(item.ItemType || 'book')}</td>
<td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td> <td style="font-weight:600; text-align:center;">${item.Quantity || item.GroupedDisplayCount || 1}</td>
<td> <td>
@@ -1565,197 +1565,8 @@
}).join(''); }).join('');
} }
window.openEditLibraryItem = async function (itemId) { function openEditLibraryItem(itemId) {
const item = libraryItems.find(i => i._id === itemId); window.location.href = `/item_edit/${itemId}`;
if (!item) return;
editLibraryState.itemId = item._id;
editLibraryState.seriesGroupId = item.SeriesGroupId || '';
editLibraryState.groupMembers = [];
// Felder befüllen
document.getElementById('editLibraryItemId').value = item._id;
document.getElementById('editLibraryName').value = item.Name;
document.getElementById('editLibraryType').value = item.ItemType;
document.getElementById('editLibraryIsbn').value = item.ISBN || '';
document.getElementById('editLibraryDescription').value = item.Beschreibung;
// Orte laden und bestehenden Ort setzen
await loadEditLocationOptions(item.Ort);
const codesContainer = document.getElementById('editLibraryCodesContainer');
if (codesContainer) {
codesContainer.innerHTML = '<div style="padding:10px 0; color:#6b7280;">Lade Codes...</div>';
}
const groupMembers = item.SeriesGroupId ? await fetchLibraryGroupMembers(item.SeriesGroupId) : [item];
renderLibraryGroupCodeFields(groupMembers.length > 0 ? groupMembers : [item], item._id);
document.getElementById('editLibraryModal').style.display = 'flex';
};
function closeEditLibraryModal() {
document.getElementById('editLibraryModal').style.display = 'none';
// Setzt das Feld für neuen Ort sicherheitshalber zurück, wenn geschlossen wird
cancelEditAddLocation();
}
// Lädt die Optionen in das Dropdown und wählt den aktuellen Ort des Mediums aus
async function loadEditLocationOptions(currentLocation) {
try {
const response = await fetch('/get_predefined_locations');
const data = await response.json();
const ortSelect = document.getElementById('editLibraryLocation');
if (ortSelect) {
// Bestehende Optionen löschen (außer die erste Platzhalter-Option)
while (ortSelect.children.length > 1) {
ortSelect.removeChild(ortSelect.lastChild);
}
let locationFound = false;
// Neue Optionen aus der Datenbank hinzufügen
data.locations.forEach(location => {
const option = document.createElement('option');
option.value = location;
option.textContent = location;
ortSelect.appendChild(option);
if (location === currentLocation) {
locationFound = true;
}
});
// WICHTIG: Falls der aktuell hinterlegte Ort nicht in der Datenbank-Liste ist,
// fügen wir ihn als temporäre Option hinzu, damit er nicht beim Speichern überschrieben wird.
if (currentLocation && !locationFound) {
const option = document.createElement('option');
option.value = currentLocation;
option.textContent = currentLocation;
ortSelect.appendChild(option);
}
// Zuletzt den aktuellen Ort anwählen
if (currentLocation) {
ortSelect.value = currentLocation;
}
}
} catch (error) {
console.error('Error loading location options:', error);
}
}
// Funktion um einen neuen Ort ins Bearbeiten-Dropdown aufzunehmen
function addEditNewLocation() {
const newLocationInput = document.getElementById('edit-new-location-input');
const newLocation = newLocationInput.value.trim();
if (!newLocation) {
alert('Bitte geben Sie einen Ort ein.');
return;
}
// Füge Option dem Dropdown hinzu
const ortSelect = document.getElementById('editLibraryLocation');
const option = document.createElement('option');
option.value = newLocation;
option.textContent = newLocation;
ortSelect.appendChild(option);
// Wähle die neue Option direkt aus
ortSelect.value = newLocation;
// Verstecke die Eingabemaske wieder
cancelEditAddLocation();
}
// Funktion zum Abbrechen der Orteingabe
function cancelEditAddLocation() {
const container = document.getElementById('edit-new-location-container');
if(container) container.style.display = 'none';
const input = document.getElementById('edit-new-location-input');
if(input) input.value = '';
} }
</script> </script>
<div id="editLibraryModal" class="modal" style="display:none;">
<div class="modal-content" style="max-width: 760px; padding: 25px; border-radius: 8px;">
<span class="close" onclick="closeEditLibraryModal()" style="cursor: pointer; float: right; font-size: 24px;">&times;</span>
<h3 style="margin-top:0;">Bibliotheksmedium bearbeiten</h3>
<!-- Bereich für Gruppen-Informationen (Hier konsolidiert!) -->
<div id="editLibraryGroupWarning" style="display:none; background-color: #fff; padding: 15px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #0ea5e9;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; border-bottom: 1px solid #eee; padding-bottom: 10px;">
<strong style="color: #0ea5e9;">Gruppen-Range (Total: <span id="editLibraryGroupCount"></span>)</strong>
</div>
<p id="editLibraryGroupHint" style="margin: 5px 0; font-size: 12px; color: #555;">
Änderungen an Titel, Ort und Beschreibung werden auf alle Exemplare der Gruppe übertragen.
</p>
<div style="margin-top: 15px; font-size: 11px; background: #e0f2fe; padding: 8px; border-radius: 4px;">
<strong>Hinweis:</strong> Jeder Mediencode wird einzeln gespeichert, damit alle Exemplare der Gruppe korrekt bleiben.
</div>
</div>
<form id="editLibraryForm">
<input type="hidden" id="editLibraryItemId">
<div class="edit-grid">
<div class="full">
<label for="editLibraryName">Titel</label>
<input id="editLibraryName" required style="width: 100%;">
</div>
<div>
<label for="editLibraryType">Medientyp</label>
<select id="editLibraryType" style="width: 100%;">
<option value="Buch">Buch</option>
<option value="Schulbuch">Schulbuch</option>
<option value="cd">CD</option>
<option value="dvd">DVD</option>
<option value="other">Sonstige Medien</option>
</select>
</div>
<div>
<label for="editLibraryIsbn">ISBN</label>
<input id="editLibraryIsbn" placeholder="optional ISBN-10/13" style="width: 100%;">
</div>
<div>
<label>Mediencodes</label>
<div id="editLibraryCodesContainer"></div>
</div>
<!-- NEUER BEREICH FÜR DIE ORTS-AUSWAHL -->
<div class="full form-group">
<label for="editLibraryLocation">Ort</label>
<div style="display: flex; gap: 10px; margin-bottom: 5px;">
<select id="editLibraryLocation" required style="flex-grow: 1; padding: 8px;">
<option value="">-- Bitte Ort auswählen --</option>
<!-- Options will be loaded by JavaScript -->
</select>
<button type="button" class="button" id="edit-add-new-location-btn" onclick="document.getElementById('edit-new-location-container').style.display = 'flex';" style="white-space: nowrap;">
Neuen Ort hinzufügen
</button>
</div>
<div id="edit-new-location-container" class="edit-new-location-container" style="display: none; gap: 10px; margin-top: 5px;">
<input type="text" id="edit-new-location-input" placeholder="Neuen Ort eingeben" style="flex-grow: 1; padding: 8px;">
<button type="button" class="button" onclick="addEditNewLocation()" style="background:#10b981; color:#fff;">Hinzufügen</button>
<button type="button" class="button" onclick="cancelEditAddLocation()" style="background:#ef4444; color:#fff;">Abbrechen</button>
</div>
</div>
<!-- ENDE ORTS-AUSWAHL -->
<div class="full">
<label for="editLibraryDescription">Beschreibung</label>
<textarea id="editLibraryDescription" rows="4" required style="width: 100%;"></textarea>
</div>
</div>
<div class="edit-actions" style="margin-top:20px;">
<button type="submit" class="button" style="background:#0ea5e9;color:#fff;">Speichern & Synchronisieren</button>
<button type="button" class="button" onclick="closeEditLibraryModal()">Abbrechen</button>
</div>
</form>
</div>
</div>
{% endblock %} {% endblock %}