Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f314b5ae70 | |||
| 77c4e1b4c5 | |||
| e17f098c47 | |||
| f41acc78cd | |||
| a7a5341961 | |||
| 9ace2e2e5e | |||
| dcd1529377 | |||
| ae80be31e7 |
+291
-711
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
from typing import Dict, Any
|
||||
|
||||
class ModuleRegistry:
|
||||
def __init__(self):
|
||||
self._modules: Dict[str, Any] = {}
|
||||
self._path_matchers = {}
|
||||
|
||||
def register(self, name: str, settings_bool, path_matcher=lambda path: False):
|
||||
self._modules[name] = settings_bool
|
||||
self._path_matchers[name] = path_matcher
|
||||
|
||||
def is_enabled(self, name: str) -> bool:
|
||||
if name not in self._modules:
|
||||
return False
|
||||
return bool(self._modules[name])
|
||||
|
||||
def get_all_status(self) -> Dict[str, bool]:
|
||||
return {name: bool(sys_bool) for name, sys_bool in self._modules.items()}
|
||||
|
||||
def get_module_for_path(self, path: str) -> str:
|
||||
for name, matcher in self._path_matchers.items():
|
||||
if self.is_enabled(name) and matcher(path):
|
||||
return name
|
||||
return None
|
||||
|
||||
registry = ModuleRegistry()
|
||||
@@ -668,3 +668,123 @@ def generate_audit_pdf(verify_result, event_counts, audit_rows, export_type='off
|
||||
return pdf_gen.generate_quick_check(verify_result, event_counts, audit_rows)
|
||||
else:
|
||||
return pdf_gen.generate_official_report(verify_result, event_counts, audit_rows)
|
||||
|
||||
def _build_invoice_pdf(invoice_data):
|
||||
"""Render a PDF invoice for a damaged borrowed item."""
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.colors import HexColor, black, white
|
||||
from reportlab.lib.utils import simpleSplit
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
pdf_buffer = io.BytesIO()
|
||||
c = canvas.Canvas(pdf_buffer, pagesize=A4)
|
||||
page_width, page_height = A4
|
||||
|
||||
margin_x = 20 * mm
|
||||
margin_top = 20 * mm
|
||||
usable_width = page_width - (2 * margin_x)
|
||||
current_y = page_height - margin_top
|
||||
|
||||
dark_color = HexColor('#0F172A')
|
||||
accent_color = HexColor('#B91C1C')
|
||||
light_color = HexColor('#F8FAFC')
|
||||
border_color = HexColor('#CBD5E1')
|
||||
text_color = HexColor('#1E293B')
|
||||
muted_color = HexColor('#64748B')
|
||||
|
||||
def draw_wrapped_lines(text, x_pos, y_pos, width, font_name='Helvetica', font_size=11, leading=14, color=text_color):
|
||||
if not text:
|
||||
return y_pos
|
||||
c.setFont(font_name, font_size)
|
||||
c.setFillColor(color)
|
||||
for line in simpleSplit(str(text), font_name, font_size, width):
|
||||
c.drawString(x_pos, y_pos, line)
|
||||
y_pos -= leading
|
||||
return y_pos
|
||||
|
||||
def draw_label_value(label, value, x_pos, y_pos, label_width=45 * mm):
|
||||
c.setFont('Helvetica-Bold', 10)
|
||||
c.setFillColor(muted_color)
|
||||
c.drawString(x_pos, y_pos, label)
|
||||
c.setFont('Helvetica', 10)
|
||||
c.setFillColor(text_color)
|
||||
c.drawString(x_pos + label_width, y_pos, str(value or '-'))
|
||||
return y_pos - 7 * mm
|
||||
|
||||
c.setFillColor(light_color)
|
||||
c.rect(0, 0, page_width, page_height, fill=1, stroke=0)
|
||||
|
||||
c.setFillColor(dark_color)
|
||||
c.rect(0, page_height - 28 * mm, page_width, 28 * mm, fill=1, stroke=0)
|
||||
c.setFillColor(white)
|
||||
c.setFont('Helvetica-Bold', 20)
|
||||
c.drawString(margin_x, page_height - 16 * mm, 'RECHNUNG')
|
||||
c.setFont('Helvetica', 10)
|
||||
c.drawString(margin_x, page_height - 23 * mm, 'Inventarsystem - Schadensersatz für zerstörtes Ausleihobjekt')
|
||||
|
||||
current_y = page_height - 40 * mm
|
||||
c.setStrokeColor(border_color)
|
||||
c.setLineWidth(1)
|
||||
c.line(margin_x, current_y, page_width - margin_x, current_y)
|
||||
current_y -= 12 * mm
|
||||
|
||||
invoice_number = invoice_data.get('invoice_number', '-')
|
||||
created_at = invoice_data.get('created_at_display', '-')
|
||||
borrower = invoice_data.get('borrower', '-')
|
||||
item_name = invoice_data.get('item_name', '-')
|
||||
item_code = invoice_data.get('item_code', '-')
|
||||
item_id = invoice_data.get('item_id', '-')
|
||||
damage_reason = invoice_data.get('damage_reason', '-')
|
||||
amount_text = invoice_data.get('amount_text', '-')
|
||||
|
||||
current_y = draw_label_value('Rechnungsnummer:', invoice_number, margin_x, current_y)
|
||||
current_y = draw_label_value('Datum:', created_at, margin_x, current_y)
|
||||
current_y = draw_label_value('Empfänger:', borrower, margin_x, current_y)
|
||||
current_y = draw_label_value('Ausleihe / Element:', item_name, margin_x, current_y)
|
||||
current_y = draw_label_value('Element-ID:', item_id, margin_x, current_y)
|
||||
current_y = draw_label_value('Code:', item_code, margin_x, current_y)
|
||||
current_y = current_y - 3 * mm
|
||||
|
||||
c.setFillColor(dark_color)
|
||||
c.setFont('Helvetica-Bold', 12)
|
||||
c.drawString(margin_x, current_y, 'Schadensbeschreibung')
|
||||
current_y -= 6 * mm
|
||||
current_y = draw_wrapped_lines(damage_reason, margin_x, current_y, usable_width, font_size=10, leading=13, color=text_color)
|
||||
current_y -= 4 * mm
|
||||
|
||||
c.setFillColor(dark_color)
|
||||
c.setFont('Helvetica-Bold', 12)
|
||||
c.drawString(margin_x, current_y, 'Rechnungsbetrag')
|
||||
current_y -= 8 * mm
|
||||
|
||||
c.setFillColor(accent_color)
|
||||
c.setStrokeColor(accent_color)
|
||||
c.rect(margin_x, current_y - 12 * mm, usable_width, 16 * mm, fill=1, stroke=0)
|
||||
c.setFillColor(white)
|
||||
c.setFont('Helvetica-Bold', 16)
|
||||
c.drawString(margin_x + 5 * mm, current_y - 2 * mm, amount_text)
|
||||
current_y -= 20 * mm
|
||||
|
||||
current_y = draw_wrapped_lines(
|
||||
'Bitte begleichen Sie diesen Betrag zeitnah bei der Verwaltung. Der Betrag ergibt sich aus dem zerstörten Ausleihobjekt und der dokumentierten Schadensmeldung.',
|
||||
margin_x,
|
||||
current_y,
|
||||
usable_width,
|
||||
font_size=10,
|
||||
leading=13,
|
||||
color=muted_color,
|
||||
)
|
||||
|
||||
footer_y = 18 * mm
|
||||
c.setStrokeColor(border_color)
|
||||
c.setLineWidth(0.8)
|
||||
c.line(margin_x, footer_y + 8 * mm, page_width - margin_x, footer_y + 8 * mm)
|
||||
c.setFillColor(muted_color)
|
||||
c.setFont('Helvetica', 9)
|
||||
c.drawString(margin_x, footer_y, 'Inventarsystem - Rechnungserstellung')
|
||||
c.drawRightString(page_width - margin_x, footer_y, f'{amount_text}')
|
||||
|
||||
c.save()
|
||||
pdf_buffer.seek(0)
|
||||
return pdf_buffer
|
||||
+22
-1
@@ -205,11 +205,32 @@ class _TenantAwareBool:
|
||||
return f"_TenantAwareBool(module_name={self.module_name!r}, value={self.resolve()!r})"
|
||||
|
||||
|
||||
from module_registry import registry as MODULES
|
||||
|
||||
INVENTORY_MODULE_ENABLED = _TenantAwareBool('inventory', _get(_conf, ['modules', 'inventory', 'enabled'], DEFAULTS['modules']['inventory']['enabled']))
|
||||
LIBRARY_MODULE_ENABLED = _TenantAwareBool('library', _get(_conf, ['modules', 'library', 'enabled'], DEFAULTS['modules']['library']['enabled']))
|
||||
STUDENT_CARDS_MODULE_ENABLED = _TenantAwareBool('student_cards', _get(_conf, ['modules', 'student_cards', 'enabled'], DEFAULTS['modules']['student_cards']['enabled']))
|
||||
|
||||
def _match_inventory(path):
|
||||
if not path: return False
|
||||
if path == '/' or path.startswith('/home'): return True
|
||||
return path.startswith(('/scanner', '/inventory', '/upload_admin', '/manage_filters', '/manage_locations', '/admin_borrowings', '/admin_damaged_items', '/admin/borrowings', '/admin/damaged_items', '/terminplan'))
|
||||
|
||||
def _match_library(path):
|
||||
if not path: return False
|
||||
return path.startswith(('/library', '/library_', '/student_cards'))
|
||||
|
||||
def _match_student_cards(path):
|
||||
if not path: return False
|
||||
return path.startswith(('/student_cards'))
|
||||
|
||||
# Register core modules into the pipeline
|
||||
MODULES.register('inventory', INVENTORY_MODULE_ENABLED, _match_inventory)
|
||||
MODULES.register('library', LIBRARY_MODULE_ENABLED, _match_library)
|
||||
MODULES.register('student_cards', STUDENT_CARDS_MODULE_ENABLED, _match_student_cards)
|
||||
|
||||
STUDENT_DEFAULT_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'default_borrow_days'], DEFAULTS['modules']['student_cards']['default_borrow_days']))
|
||||
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ['modules', 'student_cards', 'max_borrow_days'], DEFAULTS['modules']['student_cards']['max_borrow_days']))
|
||||
STUDENT_MAX_BORROW_DAYS = int(_get(_conf, ["modules", "student_cards", "max_borrow_days"], DEFAULTS["modules"]["student_cards"]["max_borrow_days"]))
|
||||
|
||||
# Upload/paths
|
||||
ALLOWED_EXTENSIONS = set(_get(_conf, ['allowed_extensions'], DEFAULTS['upload']['allowed_extensions']))
|
||||
|
||||
@@ -20,28 +20,12 @@
|
||||
<h4 style="margin:0 0 10px 0; color:#1a1a1a;">📄 PDF-Export (DIN 5008 konform)</h4>
|
||||
<p style="margin:0 0 10px 0; font-size:0.9rem; color:#666;">Professionelle Berichte für Schulträger und Behörden</p>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<a class="btn btn-primary" href="{{ url_for('admin_audit_export_pdf_quick') }}" style="flex:1; text-align:center;">
|
||||
🚀 Schnell-Check (kompakt)
|
||||
</a>
|
||||
<a class="btn btn-primary" href="{{ url_for('admin_audit_export_pdf_official') }}" style="flex:1; text-align:center;">
|
||||
📋 Amtlicher Bericht (DIN 5008)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Other Formats Section -->
|
||||
<div style="padding:14px; border:1px solid #e2e8f0; border-radius:10px; background: var(--ui-surface);">
|
||||
<h4 style="margin:0 0 10px 0; color:#1a1a1a;">📊 Weitere Formate</h4>
|
||||
<p style="margin:0 0 10px 0; font-size:0.9rem; color:#666;">Technische Exporte für System-Integration</p>
|
||||
<div style="display:flex; gap:8px; flex-wrap:wrap;">
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin_audit_export', format='md') }}" style="flex:1; text-align:center;">
|
||||
📝 Markdown
|
||||
</a>
|
||||
<a class="btn btn-outline-secondary" href="{{ url_for('admin_audit_export', format='json') }}" style="flex:1; text-align:center;">
|
||||
⚙️ JSON
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:repeat(auto-fit,minmax(220px,1fr)); gap:12px; margin:16px 0 20px;">
|
||||
|
||||
@@ -459,7 +459,7 @@
|
||||
<div id="schedule-modal" class="item-modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal" onclick="closeScheduleModal()">×</span>
|
||||
<h2>Termin planen</h2>
|
||||
<h2>Reservieren</h2>
|
||||
<form id="schedule-form">
|
||||
<input type="hidden" id="schedule-item-id" name="item_id">
|
||||
|
||||
@@ -1057,7 +1057,7 @@
|
||||
:
|
||||
`<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
|
||||
}
|
||||
${canScheduleItem ? `<button class="schedule-button" type="button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
${canScheduleItem ? `<button class="schedule-button" type="button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
|
||||
<button class="quick-details-button" type="button" onclick="openItemQuick('${item._id}')">Details</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -1734,7 +1734,7 @@
|
||||
</form>`
|
||||
: `<button class="ausleihen disabled-button" disabled>${item.BlockedNow ? 'Reserviert' : 'Ausgeliehen'}</button>`
|
||||
}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
|
||||
<button type="button" class="details-button" onclick="document.getElementById('item-modal').style.display='none';">Schließen</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -2643,7 +2643,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<div id="schedule-modal" class="item-modal">
|
||||
<div class="modal-content">
|
||||
<span class="close-modal" onclick="closeScheduleModal()">×</span>
|
||||
<h2>Termin planen</h2>
|
||||
<h2>Reservieren</h2>
|
||||
<form id="schedule-form">
|
||||
<input type="hidden" id="schedule-item-id" name="item_id">
|
||||
<div class="form-group" id="schedule-specific-item-group" style="display:none;">
|
||||
@@ -3654,7 +3654,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
</form>
|
||||
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-card-${item._id}')">Bearbeiten</button>
|
||||
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
|
||||
</div>
|
||||
`;
|
||||
itemsContainer.appendChild(card);
|
||||
@@ -4531,7 +4531,7 @@ document.addEventListener('DOMContentLoaded', ()=>{
|
||||
<button class="edit-button" onclick="openEditModalForSelectedUnit('${item._id}', 'specific-item-modal-${item._id}')">Bearbeiten</button>
|
||||
<button class="duplicate-button" onclick="duplicateItem('${item._id}')">Duplizieren</button>
|
||||
${damageReports.length > 0 ? `<button class="damage-button" onclick="markDamageAsRepaired('${item._id}')">Repariert</button>` : `<button class="damage-button" onclick="registerDamage('${item._id}')">Schaden melden</button>`}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Termin planen</button>` : ''}
|
||||
${canScheduleItem ? `<button class="schedule-button" onclick="openScheduleModal('${item._id}')">Reservieren</button>` : ''}
|
||||
<form method="POST" action="/delete_item/${item._id}" style="display:inline;" onsubmit="return confirm('Sind Sie sicher?')">
|
||||
<button class="delete-button" type="submit">Löschen</button>
|
||||
</form>
|
||||
|
||||
@@ -96,6 +96,10 @@ def get_tenant_config(tenant_id=None):
|
||||
|
||||
if tenant_id in TENANT_REGISTRY:
|
||||
return TENANT_REGISTRY[tenant_id] or {}
|
||||
|
||||
for alias in _tenant_db_aliases(tenant_id):
|
||||
if alias in TENANT_REGISTRY:
|
||||
return TENANT_REGISTRY[alias] or {}
|
||||
|
||||
return TENANT_REGISTRY.get('default', {}) or {}
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from app import app
|
||||
with app.test_client() as client:
|
||||
with client.session_transaction() as sess:
|
||||
sess['username'] = 'admin'
|
||||
sess['admin'] = True
|
||||
resp = client.get('/terminplan')
|
||||
print("Status:", resp.status_code)
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""CLI utility to verify the tamper-evident audit chain."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
|
||||
import settings as cfg
|
||||
from settings import MongoClient
|
||||
import audit_log as al
|
||||
|
||||
|
||||
def main():
|
||||
client = None
|
||||
try:
|
||||
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
|
||||
db = client[cfg.MONGODB_DB]
|
||||
al.ensure_audit_indexes(db)
|
||||
result = al.verify_audit_chain(db)
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2, default=str))
|
||||
return 0 if result.get("ok") else 2
|
||||
except Exception as exc:
|
||||
print(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
|
||||
return 1
|
||||
finally:
|
||||
if client:
|
||||
client.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user