v4.1.0: DMS email documents, category-specific Nachweis linking, version system
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled

- Save cover email body as DMS document with new 'email' context type
- Show email body separately from attachments in email detail view
- Add per-category DMS document assignment in quarterly confirmation
  (Studiennachweis, Einkommenssituation, Vermögenssituation)
- Add VERSION file and context processor for automatic version display
- Add MCP server, agent system, import/export, and new migrations
- Update compose files and production environment template

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
SysAdmin Agent
2026-03-15 18:48:52 +00:00
parent faeb7c1073
commit e0b377014c
49 changed files with 5913 additions and 55 deletions

View File

@@ -0,0 +1,208 @@
{% extends 'base.html' %}
{% block title %}CSV Import Feldzuordnung{% endblock %}
{% block content %}
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="fas fa-columns text-primary"></i> Feldzuordnung: {{ import_label }}</h1>
<a href="{% url 'stiftung:import_export_hub' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Zurück
</a>
</div>
<div class="alert alert-info">
<i class="fas fa-file-csv"></i>
<strong>{{ filename }}</strong> {{ total_rows }} Datenzeilen erkannt.
Ordnen Sie die CSV-Spalten den Datenbankfeldern zu. Nicht zugeordnete Spalten werden übersprungen.
</div>
<form method="post" action="{% url 'stiftung:csv_import_execute' %}">
{% csrf_token %}
<!-- Mapping Table -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-project-diagram"></i> Spalten zuordnen
</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 5%;">#</th>
<th style="width: 25%;">CSV-Spalte</th>
<th style="width: 5%;"></th>
<th style="width: 30%;">Zuordnung</th>
<th style="width: 35%;">Vorschau</th>
</tr>
</thead>
<tbody>
{% for col in header_previews %}
<tr>
<td class="text-muted">{{ forloop.counter }}</td>
<td><code>{{ col.header }}</code></td>
<td class="text-center text-muted"><i class="fas fa-arrow-right"></i></td>
<td>
<select name="mapping_{{ forloop.counter0 }}" class="form-select form-select-sm mapping-select"
data-col-index="{{ forloop.counter0 }}">
<option value="__skip__"> Überspringen </option>
{% for field in model_fields %}
<option value="{{ field.1 }}">
{{ field.0 }}{% if field.3 %} *{% endif %}
({{ field.2 }})
</option>
{% endfor %}
</select>
</td>
<td>
<small class="text-muted">{{ col.preview|truncatechars:60 }}</small>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card-footer">
<small class="text-muted">* = Pflichtfeld. Zuordnungen werden automatisch vorgeschlagen und können manuell geändert werden.</small>
</div>
</div>
<!-- Preview Table -->
{% if preview_rows %}
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Vorschau (erste {{ preview_rows|length }} Zeilen)
</h5>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-striped mb-0">
<thead class="table-light">
<tr>
{% for col in header_previews %}
<th><small>{{ col.header }}</small></th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in preview_rows %}
<tr>
{% for cell in row %}
<td><small>{{ cell|truncatechars:40 }}</small></td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- Import Mode -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-light">
<h5 class="card-title mb-0">
<i class="fas fa-cog"></i> Import-Modus
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="radio" name="import_mode" id="mode_skip" value="skip" checked>
<label class="form-check-label" for="mode_skip">
<strong>Nur neue importieren</strong>
<br><small class="text-muted">Bereits vorhandene Einträge werden übersprungen</small>
</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="radio" name="import_mode" id="mode_merge" value="merge">
<label class="form-check-label" for="mode_merge">
<strong>Zusammenführen</strong>
<br><small class="text-muted">Vorhandene Einträge werden mit neuen Daten aktualisiert</small>
</label>
</div>
</div>
<div class="col-md-4">
<div class="form-check">
<input class="form-check-input" type="radio" name="import_mode" id="mode_create" value="create">
<label class="form-check-label" for="mode_create">
<strong>Alle neu anlegen</strong>
<br><small class="text-muted">Keine Duplikatprüfung, alle Zeilen als neue Einträge</small>
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="d-flex justify-content-between">
<a href="{% url 'stiftung:import_export_hub' %}" class="btn btn-outline-secondary">
<i class="fas fa-times"></i> Abbrechen
</a>
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-upload"></i> {{ total_rows }} Zeilen importieren
</button>
</div>
</form>
</div>
</div>
{% endblock %}
{% block javascript %}
<script>
// Auto-apply mapping from server
const autoMapping = {{ auto_mapping_json|safe }};
document.addEventListener('DOMContentLoaded', function() {
for (const [colIdx, fieldName] of Object.entries(autoMapping)) {
const select = document.querySelector(`select[name="mapping_${colIdx}"]`);
if (select) {
for (const opt of select.options) {
if (opt.value === fieldName) {
opt.selected = true;
break;
}
}
}
}
// Highlight duplicate mappings
const selects = document.querySelectorAll('.mapping-select');
selects.forEach(sel => {
sel.addEventListener('change', highlightDuplicates);
});
highlightDuplicates();
});
function highlightDuplicates() {
const selects = document.querySelectorAll('.mapping-select');
const valueCount = {};
selects.forEach(sel => {
const val = sel.value;
if (val && val !== '__skip__') {
valueCount[val] = (valueCount[val] || 0) + 1;
}
});
selects.forEach(sel => {
const val = sel.value;
if (val && val !== '__skip__' && valueCount[val] > 1) {
sel.classList.add('is-invalid');
} else {
sel.classList.remove('is-invalid');
}
});
}
</script>
{% endblock %}