feat(inventory): add repair resolution workflow and invoice correction logic
Release Inventarsystem / release-docker (push) Successful in 2m14s

- 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
This commit is contained in:
2026-08-19 22:05:32 +02:00
parent 78c215b234
commit cd03bbd2e5
2 changed files with 279 additions and 122 deletions
+189 -71
View File
@@ -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/<borrow_id>/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/<borrow_id>/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/<item_id>/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/<borrow_id>/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/<item_id>/invoices', methods=['GET'])
def library_item_invoices(item_id):
"""Show all stored invoices for one specific library item."""