Compare commits

...

24 Commits

Author SHA1 Message Date
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
Aiirondev_dev 08bea97f0f 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:49:20 +02:00
Aiirondev_dev 9164cd030d Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 23:07:13 +02:00
Aiirondev_dev 9227392787 Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-11 22:56:40 +02:00
Aiirondev_dev 4917c22ae3 Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 22:49:40 +02:00
Aiirondev_dev a2f2dd5a9e Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 22:41:30 +02:00
Aiirondev_dev faf270ff93 Debug changes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-11 22:35:43 +02:00
Aiirondev_dev beeb562ac4 Dubvug changes
Release Inventarsystem / release-docker (push) Successful in 2m32s
2026-08-11 22:21:28 +02:00
Aiirondev_dev 0199957545 Dubvug changes
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 21:35:08 +02:00
Aiirondev_dev a518adb054 Dubvug changes
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-11 21:27:39 +02:00
Aiirondev_dev 6a94d50d28 Additiopn to filter out the iduvidual items and
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 21:16:37 +02:00
Aiirondev_dev c90cef6dcf Addition of ort preloading for the edeting modal
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 15:30:26 +02:00
Aiirondev_dev b5451a4ef0 Addition of ort preloading for the edeting modal
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-08-11 15:12:17 +02:00
Aiirondev_dev a4afef8283 Fix of a stray redirect error
Release Inventarsystem / release-docker (push) Successful in 2m17s
2026-08-11 15:00:20 +02:00
Aiirondev_dev b2951eed6c Fix of broken authentication metode
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 10:46:38 +02:00
Aiirondev_dev 9a37c047c1 Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-11 02:01:27 +02:00
Aiirondev_dev 7290fb4ed1 Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 01:52:06 +02:00
Aiirondev_dev ee9ef3df6f Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Has been cancelled
2026-08-11 01:09:53 +02:00
Aiirondev_dev 9f3799a77f Fix of an missing parethasys
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-11 00:57:51 +02:00
6 changed files with 1941 additions and 630 deletions
+204 -4
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
@@ -3313,7 +3313,8 @@ def api_library_items():
ausleihungen_db = db['ausleihungen'] ausleihungen_db = db['ausleihungen']
query = { query = {
'ItemType': {'$in': ['book', 'cd', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']}, 'ItemType': {'$in': ['book', 'cd', 'CD', 'DVD', 'dvd', 'schoolbook', 'schulbuch', 'Buch', 'Schulbuch']},
'IsGroupedSubItem': {'$ne': True},
'Deleted': {'$ne': True} 'Deleted': {'$ne': True}
} }
@@ -3787,6 +3788,7 @@ def api_item_detail(item_id):
<h2>{html.escape(str(item.get('Name', 'Untitled')))}</h2> <h2>{html.escape(str(item.get('Name', 'Untitled')))}</h2>
<p><strong>ISBN:</strong> {html.escape(str(item.get('ISBN', item.get('Code4', '-'))))}</p> <p><strong>ISBN:</strong> {html.escape(str(item.get('ISBN', item.get('Code4', '-'))))}</p>
<p><strong>Anzahl:</strong> {html.escape(str(item.get('SeriesCount', '-')))}</p> <p><strong>Anzahl:</strong> {html.escape(str(item.get('SeriesCount', '-')))}</p>
<p><strong>Code:</strong> {html.escape(str(item.get('Code_4', '-')))}</p>
<p><strong>Ort:</strong> {html.escape(str(item.get('Ort', '-')))}</p> <p><strong>Ort:</strong> {html.escape(str(item.get('Ort', '-')))}</p>
<p><strong>Typ:</strong> {html.escape(str(item.get('ItemType', '-')))}</p> <p><strong>Typ:</strong> {html.escape(str(item.get('ItemType', '-')))}</p>
<p><strong>Kategorie:</strong> {html.escape(str(item.get('library_category', '-')))}</p> <p><strong>Kategorie:</strong> {html.escape(str(item.get('library_category', '-')))}</p>
@@ -5779,7 +5781,7 @@ def upload_item():
app.logger.warning('Audit write failed for library_item_created') app.logger.warning('Audit write failed for library_item_created')
flash(success_msg, 'success') flash(success_msg, 'success')
return redirect(url_for(success_redirect_endpoint, highlight_item=str(item_id))) return redirect(url_for(success_redirect_endpoint))
else: else:
error_msg = 'Fehler beim Hinzufügen des Elements' error_msg = 'Fehler beim Hinzufügen des Elements'
if is_mobile: if is_mobile:
@@ -6365,6 +6367,204 @@ 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).
"""
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'))
# --- GET: Template anzeigen ---
if request.method == 'GET':
item = it.get_item(id)
if not item:
flash('Element nicht gefunden.', 'error')
return redirect(url_for('home_admin'))
item['_id'] = str(item['_id'])
# Codes für die Ansicht trennen (Basis-Code vs. Einzelcodes)
base_code = item.get('Code_4', '')
individual_codes = []
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:
g_item = it.get_item(gid)
c4 = g_item.get('Code_4')
if c4 and c4 != base_code:
individual_codes.append(c4)
item['IndividualCodes'] = '\n'.join(individual_codes)
show_library = cfg.MODULES.is_enabled('library')
return render_template(
'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', '')}"
)
# --- POST: Speichern ---
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
name = sanitize_form_value(request.form.get('name'))
ort = sanitize_form_value(request.form.get('ort'))
beschreibung = sanitize_form_value(request.form.get('beschreibung'))
# Basis-Code & Einzelcodes aus der Textarea
code_4 = sanitize_form_value(request.form.get('code_4'))
individual_codes_raw = request.form.get('individual_codes', '')
individual_codes = []
for c in individual_codes_raw.replace('\r', '').split('\n'):
clean_c = sanitize_form_value(c)
if clean_c and clean_c != code_4 and clean_c not in individual_codes:
individual_codes.append(clean_c)
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)
# 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]
items_col = db_instance['items']
# Bilder verarbeiten (GridFS)
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}")
has_code_error = False
for code in all_codes_to_check:
if not code:
continue
existing = items_col.find_one({'Code_4': code, 'Deleted': {'$ne': True}})
if existing:
is_same_item = str(existing['_id']) == str(id)
is_in_same_group = current_group_id and existing.get('SeriesGroupId') == current_group_id
if not is_same_item and not is_in_same_group:
flash(f'Der Code "{code}" wird bereits von einem anderen Artikel verwendet.', 'error')
has_code_error = True
break
client.close()
if has_code_error:
return redirect(redirect_target)
# 1. Gruppen-Bestand über die korrigierte Helfer-Funktion synchronisieren
it.sync_group_codes(str(id), code_4, individual_codes)
# 2. Deine bestehende update_item Funktion aufrufen
success = it.update_item(
id=str(id),
name=name,
ort=ort,
beschreibung=beschreibung,
images=images,
verfuegbar=verfuegbar,
filter1=filter1,
filter2=filter2,
filter3=filter3,
ansch_jahr=anschaffungs_jahr,
ansch_kost=anschaffungs_kosten,
code_4=code_4,
reservierbar=reservierbar,
isbn=isbn_raw,
item_type=item_type_input
)
if success:
flash('Artikel erfolgreich aktualisiert.', 'success')
else:
flash('Fehler beim Aktualisieren des Artikels.', 'error')
return redirect(redirect_target)
@app.route('/update_group', methods=['POST']) @app.route('/update_group', methods=['POST'])
def update_group(): def update_group():
@@ -7696,7 +7896,7 @@ def user_del():
last_name = "" last_name = ""
fullname = None fullname = None
users_list.append({ users_list.append({
'username': username, 'username': decrypt_text(username),
'admin': user.get('Admin', False), 'admin': user.get('Admin', False),
'fullname': fullname, 'fullname': fullname,
'name': name, 'name': name,
+120 -1
View File
@@ -20,6 +20,7 @@ Collection Structure:
""" """
from bson.objectid import ObjectId from bson.objectid import ObjectId
from bson.errors import InvalidId from bson.errors import InvalidId
import uuid
import datetime import datetime
import Web.modules.database.settings as cfg import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient from Web.modules.database.settings import MongoClient
@@ -1147,4 +1148,122 @@ def get_current_status(item_id, decrypt=True):
return None return None
except Exception as e: except Exception as e:
print(f"Error retrieving current status for item {item_id}: {e}") print(f"Error retrieving current status for item {item_id}: {e}")
return None return None
def sync_group_codes(primary_obj_id, base_code, individual_codes_list):
"""
Synchronisiert die Barcodes einer Gruppe im korrekten Schema
(angelehnt an das 'Augenmodell groß'-Vorbild).
"""
if not base_code:
return False
# 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]
items = db['items']
primary_item = items.find_one({'_id': ObjectId(primary_obj_id)})
if not primary_item:
client.close()
return False
group_id = primary_item.get('SeriesGroupId')
# 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())
# 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
}}
)
# 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'])}
# 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']
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)
# 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']})
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
except Exception as e:
print(f"Error syncing group codes: {e}")
return False
+1 -1
View File
@@ -650,7 +650,7 @@ def add_user(
safe_last_name = last_name.strip() if last_name else '' safe_last_name = last_name.strip() if last_name else ''
user_doc = { user_doc = {
'Username': dp.encrypt_text(username), 'Username': username,
'Password': hashing(password), 'Password': hashing(password),
'Admin': (permission_preset == "full_access"), 'Admin': (permission_preset == "full_access"),
'active_ausleihung': None, 'active_ausleihung': None,
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+56
View File
@@ -4315,6 +4315,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
hiddenInput.value = image; hiddenInput.value = image;
editForm.appendChild(hiddenInput); editForm.appendChild(hiddenInput);
} }
}); });
} }
@@ -5632,6 +5633,61 @@ document.addEventListener('DOMContentLoaded', ()=>{
futureAppointments.sort((a, b) => new Date(a.date) - new Date(b.date)); futureAppointments.sort((a, b) => new Date(a.date) - new Date(b.date));
return futureAppointments[0]; return futureAppointments[0];
} }
// Load location options
function loadLocationOptions() {
fetch('/get_predefined_locations')
.then(response => response.json())
.then(data => {
const ortSelect = document.getElementById('ort');
if (ortSelect) {
// Clear existing options except the first one
while (ortSelect.children.length > 1) {
ortSelect.removeChild(ortSelect.lastChild);
}
// Add new options - data.locations contains the array
data.locations.forEach(location => {
const option = document.createElement('option');
option.value = location;
option.textContent = location;
ortSelect.appendChild(option);
});
}
})
.catch(error => {
console.error('Error loading location options:', error);
});
}
// Function to add new location
function addNewLocation() {
const newLocationInput = document.getElementById('new-location-input');
const newLocation = newLocationInput.value.trim();
if (!newLocation) {
alert('Bitte geben Sie einen Ort ein.');
return;
}
// Add to dropdown
const ortSelect = document.getElementById('ort');
const option = document.createElement('option');
option.value = newLocation;
option.textContent = newLocation;
option.selected = true;
ortSelect.appendChild(option);
// Hide the input container
document.getElementById('new-location-container').style.display = 'none';
newLocationInput.value = '';
}
// Function to cancel adding new location
function cancelAddLocation() {
document.getElementById('new-location-container').style.display = 'none';
document.getElementById('new-location-input').value = '';
}
</script> </script>
<!-- Include edit item functions --> <!-- Include edit item functions -->