From cd03bbd2e5dba6a03e77c7dbe6fb92514f6c2496 Mon Sep 17 00:00:00 2001 From: AIIrondev Date: Wed, 19 Aug 2026 22:05:32 +0200 Subject: [PATCH] feat(inventory): add repair resolution workflow and invoice correction logic - Add repair resolution mechanism (`resolve_repaired_item`) allowing users to: * Just repair: Return item to normal state (`Verfuegbar: True`) * Replace: Update `Code_4` (inventory/barcode) and restore availability * Delete: Remove item and update series counts (`SeriesCount`) and parent/child references - Support `Code_4` updates in `update_item` for specific physical items - Implement invoice correction route (`admin_add_invoice_correction`) with audit logging - Add modal UI in library admin view to handle repair actions and code replacements --- Web/app.py | 260 ++++++++++++++------ Web/templates/library_borrowings_admin.html | 141 +++++++---- 2 files changed, 279 insertions(+), 122 deletions(-) diff --git a/Web/app.py b/Web/app.py index c063cda..f29ea3c 100755 --- a/Web/app.py +++ b/Web/app.py @@ -6743,7 +6743,6 @@ def mark_damage_repaired(id): if result.matched_count == 0: return jsonify({'success': False, 'message': 'Objekt nicht gefunden.'}), 404 - # Best-effort system log entry for repair action try: logs_collection = db['system_logs'] logs_collection.insert_one({ @@ -8840,132 +8839,145 @@ def admin_mark_invoice_paid(borrow_id): client.close() -@app.route('/admin/borrowings//invoice/finalize', methods=['POST']) -def admin_finalize_invoice_and_repair(borrow_id): - """Mark invoice as paid and item as repaired in one action.""" +@app.route('/admin/borrowings//invoice/pay', methods=['POST']) +def admin_mark_invoice_paid(borrow_id): + """Mark invoice as paid.""" if 'username' not in session: - flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') + flash('Ihnen ist es nicht gestattet, versuchen sie es erneut nach Anmeldung!', 'error') return redirect(url_for('login')) current_permissions = us.get_effective_permissions(session['username']) - if not current_permissions['pages'].get('admin_borrowings', False): - flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') - return redirect(url_for('home_admin')) + flash('Ihnen fehlen die nötigen Berechtigungen.', 'error') + return redirect(request.referrer or url_for('home_admin')) client = None try: client = MongoClient(MONGODB_HOST, MONGODB_PORT) db = client[MONGODB_DB] ausleihungen = db['ausleihungen'] - items_col = db['items'] borrow_doc = ausleihungen.find_one({'_id': ObjectId(borrow_id)}) if not borrow_doc: flash('Ausleihung nicht gefunden.', 'error') - return redirect(url_for('admin_borrowings')) + return redirect(request.referrer or url_for('admin_borrowings')) invoice_data = borrow_doc.get('InvoiceData') or {} if not invoice_data: flash('Für diese Ausleihung existiert keine Rechnung.', 'warning') - return redirect(url_for('admin_borrowings')) - - item_doc = None - item_id = borrow_doc.get('Item') - if item_id: - try: - item_doc = items_col.find_one({'_id': ObjectId(item_id)}) - except Exception: - item_doc = items_col.find_one({'_id': item_id}) + return redirect(request.referrer or url_for('admin_borrowings')) now = datetime.datetime.now() + update_fields = {'LastUpdated': now} - update_fields = { - 'LastUpdated': now, - } + # Rechnung auf bezahlt setzen if invoice_data.get('paid') is not True: update_fields.update({ 'InvoiceData.paid': True, 'InvoiceData.paid_at': now, 'InvoiceData.paid_by': session.get('username', ''), }) + + # Ausleihe abschließen, falls noch aktiv if borrow_doc.get('Status') == 'active': update_fields['Status'] = 'completed' update_fields['End'] = now ausleihungen.update_one({'_id': borrow_doc['_id']}, {'$set': update_fields}) - repaired = False - resolved_count = 0 - if item_doc: - open_reports = item_doc.get('DamageReports', []) or [] - resolved_count = len(open_reports) - item_update = { - '$set': { - 'DamageReports': [], - 'HasDamage': False, - 'Verfuegbar': True, - 'LastUpdated': now, - }, - '$unset': { - 'User': '', - 'Condition': '', - }, - } - if open_reports: - repair_entry = { - 'repaired_by': session['username'], - 'repaired_at': now, - 'resolved_reports': open_reports, - } - item_update['$push'] = {'DamageRepairs': {'$each': [repair_entry], '$position': 0}} - - item_result = items_col.update_one({'_id': item_doc['_id']}, item_update) - repaired = item_result.matched_count > 0 - + # Logging try: - logs_collection = db['system_logs'] - logs_collection.insert_one({ - 'type': 'damage_invoice_finalize', + db['system_logs'].insert_one({ + 'type': 'invoice_paid', 'timestamp': now.isoformat(), 'user': session.get('username'), 'borrow_id': borrow_id, - 'item_id': str(item_doc.get('_id')) if item_doc else '', 'invoice_number': invoice_data.get('invoice_number', ''), 'amount': invoice_data.get('amount'), - 'repaired': repaired, - 'resolved_damage_reports': resolved_count, 'ip': request.remote_addr, }) except Exception as log_err: - app.logger.warning(f"Damage invoice finalize log write failed for borrow {borrow_id}: {log_err}") + app.logger.warning(f"Log failed: {log_err}") _append_audit_event_standalone( - event_type='invoice_finalized_and_repaired', + event_type='invoice_paid', payload={ 'borrow_id': borrow_id, - 'item_id': str(item_doc.get('_id')) if item_doc else '', 'invoice_number': invoice_data.get('invoice_number', ''), 'amount': invoice_data.get('amount'), - 'repaired': repaired, - 'resolved_damage_reports': resolved_count, } ) - if repaired: - flash('Rechnung als bezahlt markiert und Element als repariert abgeschlossen.', 'success') - else: - flash('Rechnung als bezahlt markiert. Element konnte nicht repariert werden (nicht gefunden).', 'warning') - return redirect(url_for('admin_borrowings')) + flash('Rechnung erfolgreich als bezahlt markiert.', 'success') + return redirect(request.referrer or url_for('library_loans_admin')) except Exception as e: - app.logger.error(f"Error finalizing invoice/repair for borrow {borrow_id}: {e}") - flash('Fehler beim Kombinieren von bezahlt und repariert.', 'error') - return redirect(url_for('admin_borrowings')) + app.logger.error(f"Error marking invoice paid for borrow {borrow_id}: {e}") + flash('Fehler beim Markieren als bezahlt.', 'error') + return redirect(request.referrer or url_for('library_loans_admin')) finally: if client: client.close() +@app.route('/admin/items//repair', methods=['POST']) +def mark_damage_repaired(item_id): + """Mark item as repaired and make it available again.""" + if 'username' not in session: + return redirect(url_for('login')) + + client = None + try: + client = MongoClient(MONGODB_HOST, MONGODB_PORT) + db = client[MONGODB_DB] + items_col = db['items'] + + try: + obj_id = ObjectId(item_id) + except Exception: + obj_id = item_id + + item_doc = items_col.find_one({'_id': obj_id}) + if not item_doc: + flash('Element nicht gefunden.', 'error') + return redirect(request.referrer or url_for('library_loans_admin')) + + now = datetime.datetime.now() + open_reports = item_doc.get('DamageReports', []) or [] + + item_update = { + '$set': { + 'DamageReports': [], + 'HasDamage': False, + 'Verfuegbar': True, + 'LastUpdated': now, + }, + '$unset': { + 'User': '', + 'Condition': '', + }, + } + + # Historie der Reparaturen speichern + if open_reports: + repair_entry = { + 'repaired_by': session['username'], + 'repaired_at': now, + 'resolved_reports': open_reports, + } + item_update['$push'] = {'DamageRepairs': {'$each': [repair_entry], '$position': 0}} + + items_col.update_one({'_id': item_doc['_id']}, item_update) + + flash('Element wurde erfolgreich als repariert markiert und ist wieder verfügbar.', 'success') + return redirect(request.referrer or url_for('library_loans_admin')) + except Exception as e: + app.logger.error(f"Error repairing item {item_id}: {e}") + flash('Fehler bei der Reparatur.', 'error') + return redirect(request.referrer or url_for('library_loans_admin')) + finally: + if client: + client.close() + @app.route('/admin/borrowings//invoice/pdf', methods=['GET']) def admin_view_invoice_pdf(borrow_id): """View a previously created invoice PDF for a borrowing.""" @@ -9026,15 +9038,19 @@ def admin_view_invoice_pdf(borrow_id): def admin_add_invoice_correction(borrow_id): """Append an invoice correction entry without mutating the original invoice body.""" if 'username' not in session: - flash('Ihnen ist es nicht gestattet auf dieser Internetanwendung, die eben besuchte Adrrese zu nutzen, versuchen sie es erneut nach dem sie sich mit einem berechtigten Nutzer angemeldet haben!', 'error') + flash('Bitte melden Sie sich an, um auf diese Seite zuzugreifen.', 'error') return redirect(url_for('login')) current_permissions = us.get_effective_permissions(session['username']) - + if not current_permissions['pages'].get('admin_borrowings', False): flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error') return redirect(url_for('home_admin')) + if not ObjectId.is_valid(borrow_id): + flash('Ungültige Ausleih-ID.', 'error') + return redirect(url_for('admin_borrowings')) + client = None try: client = MongoClient(MONGODB_HOST, MONGODB_PORT) @@ -9064,6 +9080,7 @@ def admin_add_invoice_correction(borrow_id): now = datetime.datetime.now() correction_number = f"CORR-{now.strftime('%Y%m%d-%H%M%S')}-{str(borrow_doc.get('_id'))[-6:].upper()}" + correction_entry = { 'correction_number': correction_number, 'reason': correction_reason, @@ -9095,6 +9112,7 @@ def admin_add_invoice_correction(borrow_id): flash('Korrekturbuchung wurde revisionssicher ergänzt.', 'success') return redirect(url_for('admin_borrowings')) + except Exception as e: app.logger.error(f"Error creating invoice correction for borrow {borrow_id}: {e}") flash('Fehler beim Anlegen der Korrekturbuchung.', 'error') @@ -9104,6 +9122,106 @@ def admin_add_invoice_correction(borrow_id): client.close() +def resolve_repaired_item(item_id, action, new_code_4="", current_user="admin"): + """ + Verarbeitet Items aus der Reparatur und aktualisiert die Serien-Counts + sowie Parent/Child-Abhängigkeiten beim Löschen korrekt. + """ + if not ObjectId.is_valid(item_id): + return False, "Ungültige Item-ID." + + try: + with MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT) as client: + db = client[cfg.MONGODB_DB] + items = db['items'] + + item = items.find_one({'_id': ObjectId(item_id)}) + if not item: + return False, "Item nicht in der Datenbank gefunden." + + series_group_id = item.get('SeriesGroupId') + + # --- OPTION 1: DELETE --- + if action == 'delete': + is_parent = not item.get('IsGroupedSubItem') + + if is_parent and series_group_id: + sibling = items.find_one({ + 'SeriesGroupId': series_group_id, + '_id': {'$ne': ObjectId(item_id)}, + 'Deleted': {'$ne': True} + }) + + if sibling: + new_parent_id_str = str(sibling['_id']) + + # 1. Mache das Geschwister-Item zum neuen Parent + items.update_one( + {'_id': sibling['_id']}, + {'$set': { + 'IsGroupedSubItem': False, + 'ParentItemId': None + }} + ) + + # 2. Leite alle anderen verbleibenden Sub-Items auf den neuen Parent um + items.update_many( + { + 'SeriesGroupId': series_group_id, + '_id': {'$nin': [ObjectId(item_id), sibling['_id']]} + }, + {'$set': {'ParentItemId': new_parent_id_str}} + ) + + # Physisch aus der Datenbank löschen (Hard Delete) + items.delete_one({'_id': ObjectId(item_id)}) + + # ALTERNATIVE (Soft Delete have to see if it makes sense): + # now = datetime.datetime.now() + # items.update_one( + # {'_id': ObjectId(item_id)}, + # {'$set': {'Deleted': True, 'DeletedAt': now, 'DeletedBy': current_user}} + # ) + + # SeriesCount bei ALLEN verbleibenden Items dieser Serie um 1 reduzieren + if series_group_id: + items.update_many( + {'SeriesGroupId': series_group_id}, + {'$inc': {'SeriesCount': -1}} + ) + + return True, "Item wurde gelöscht und der Bestand aller Serien-Items angepasst." + + # --- OPTION 2: REPLACE --- + elif action == 'replace': + items.update_one( + {'_id': ObjectId(item_id)}, + {'$set': { + 'Code_4': str(new_code_4).strip(), + 'Verfuegbar': True, + 'LastUpdated': datetime.datetime.now() + }} + ) + return True, f"Item wurde ersetzt. Neuer Code 4 zugewiesen: {new_code_4}." + + # --- OPTION 3: JUST REPAIR --- + elif action == 'repair': + items.update_one( + {'_id': ObjectId(item_id)}, + {'$set': { + 'Verfuegbar': True, + 'LastUpdated': datetime.datetime.now() + }} + ) + return True, "Item wurde repariert und ist wieder regulär verfügbar." + + else: + return False, "Ungültige Aktion ausgewählt." + + except Exception as e: + print(f"Error resolving repair for item {item_id}: {e}") + return False, "Ein Datenbankfehler ist aufgetreten." + @app.route('/admin/library/items//invoices', methods=['GET']) def library_item_invoices(item_id): """Show all stored invoices for one specific library item.""" diff --git a/Web/templates/library_borrowings_admin.html b/Web/templates/library_borrowings_admin.html index e044e7b..e6840fb 100644 --- a/Web/templates/library_borrowings_admin.html +++ b/Web/templates/library_borrowings_admin.html @@ -154,6 +154,7 @@ display: flex; flex-wrap: wrap; gap: 8px; + align-items: center; } .row-actions form { @@ -196,7 +197,7 @@

Bibliotheks-Ausleihen

-

Nur Bibliotheksmedien. Hier kannst du offene Ausleihen abschließen, Rechnungen bezahlen und defekte Medien direkt zurücksetzen.

+

Nur Bibliotheksmedien. Hier kannst du offene Ausleihen abschließen, Rechnungen bezahlen und defekte Medien verwalten.

Bibliothek öffnen @@ -215,7 +216,7 @@
{{ damaged_items|length }}
- Direkt reparierbar + Direkt bearbeitbar
{{ damaged_items|length }}
@@ -311,35 +312,29 @@ {% if e.status == 'active' %} {% endif %} - {% if e.invoice_number and ((not e.invoice_paid) or e.has_damage) %} -
- -
- {% elif e.has_damage %} -
- + + {% if e.invoice_number and not e.invoice_paid %} + +
{% endif %} + {% if e.has_damage %} + + + {% endif %} + {% if e.invoice_number %} -
- - + + +
{% endif %} {% if e.status in ['active', 'planned'] %}
- +
{% endif %} @@ -392,9 +387,8 @@ -
- -
+ + {% endfor %} @@ -407,6 +401,7 @@ +