fix: align total costs with visible budgets

This commit is contained in:
2026-05-06 13:31:32 +02:00
parent 7cd85cc5ae
commit 3990a2ea49
5 changed files with 162 additions and 11 deletions
+54
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from flask import Blueprint, flash, redirect, render_template, request, url_for
from flask_login import login_required
from sqlalchemy.exc import IntegrityError
from sqlalchemy import select
from app.extensions import db
@@ -51,6 +52,9 @@ def index():
entries = db.session.scalars(
select(Entry).order_by(Entry.category_id.asc(), Entry.sort_order.asc(), Entry.name.asc())
).all()
inactive_accounts = [account for account in accounts if not account.is_active]
inactive_categories = [category for category in categories if not category.is_active]
inactive_entries = [entry for entry in entries if not entry.is_active]
return render_template(
"admin/index.html",
users=users,
@@ -58,6 +62,9 @@ def index():
accounts=accounts,
categories=categories,
entries=entries,
inactive_accounts=inactive_accounts,
inactive_categories=inactive_categories,
inactive_entries=inactive_entries,
)
@@ -263,6 +270,53 @@ def update_entry(entry_id: int):
return redirect(url_for("admin.index"))
@admin_bp.route("/accounts/<int:account_id>/delete", methods=["POST"])
@login_required
@admin_required
def delete_account(account_id: int):
account = Account.query.get_or_404(account_id)
if account.is_active:
flash("Nur inaktive Konten können hier endgültig gelöscht werden.", "danger")
return redirect(url_for("admin.index"))
try:
db.session.delete(account)
db.session.commit()
except IntegrityError:
db.session.rollback()
flash("Konto konnte nicht gelöscht werden, weil noch abhängige Daten existieren.", "danger")
return redirect(url_for("admin.index"))
flash("Konto endgültig gelöscht.", "success")
return redirect(url_for("admin.index"))
@admin_bp.route("/categories/<int:category_id>/delete", methods=["POST"])
@login_required
@admin_required
def delete_category(category_id: int):
category = Category.query.get_or_404(category_id)
if category.is_active:
flash("Nur inaktive Kategorien können hier endgültig gelöscht werden.", "danger")
return redirect(url_for("admin.index"))
db.session.delete(category)
db.session.commit()
flash("Kategorie endgültig gelöscht.", "success")
return redirect(url_for("admin.index"))
@admin_bp.route("/entries/<int:entry_id>/delete", methods=["POST"])
@login_required
@admin_required
def delete_entry(entry_id: int):
entry = Entry.query.get_or_404(entry_id)
if entry.is_active:
flash("Nur inaktive Einträge können hier endgültig gelöscht werden.", "danger")
return redirect(url_for("admin.index"))
db.session.delete(entry)
db.session.commit()
flash("Eintrag endgültig gelöscht.", "success")
return redirect(url_for("admin.index"))
@admin_bp.route("/entries/<int:entry_id>/share-rules", methods=["POST"])
@login_required
@admin_required
+17 -11
View File
@@ -190,7 +190,7 @@ class MonthService:
(
self.share_service.calculate_entry_shares(item)["internal_total"]
for item in month.entry_values
if item.entry_id not in distribution_entry_ids
if item.entry_id not in distribution_entry_ids and self._is_visible_monthly_value(item)
),
Decimal("0.00"),
)
@@ -368,19 +368,12 @@ class MonthService:
def _distribution_entry_values(self, month: Month) -> dict[str, MonthlyEntryValue]:
grouped: dict[str, list[MonthlyEntryValue]] = {}
for value in month.entry_values:
if not self._is_visible_monthly_value(value):
continue
entry = value.entry
category = entry.category if entry else None
account = category.account if category else None
if (
entry is None
or category is None
or account is None
or not entry.is_active
or not category.is_active
or not account.is_active
or account.slug not in self.allocation_service.TARGET_SLUGS
or not entry.is_allocation_target
):
if account is None or account.slug not in self.allocation_service.TARGET_SLUGS or not entry.is_allocation_target:
continue
grouped.setdefault(account.slug, []).append(value)
@@ -397,6 +390,19 @@ class MonthService:
preferred[account_slug] = values[0]
return preferred
def _is_visible_monthly_value(self, value: MonthlyEntryValue) -> bool:
entry = value.entry
category = entry.category if entry else None
account = category.account if category else None
return bool(
entry is not None
and category is not None
and account is not None
and entry.is_active
and category.is_active
and account.is_active
)
def _distribution_label(self, account_slug: str, fallback: str) -> str:
personal_labels = personal_account_names()
if account_slug in personal_labels:
+49
View File
@@ -71,4 +71,53 @@
</form>
</dialog>
{% endfor %}
<section class="panel">
<div class="panel-head">
<div>
<h2>Inaktive Elemente</h2>
<small>Ausgeblendete Konten, Kategorien und Einträge können hier endgültig gelöscht werden.</small>
</div>
</div>
{% if inactive_accounts or inactive_categories or inactive_entries %}
{% for account in inactive_accounts %}
<div class="month-row">
<div>
<strong>Konto: {{ account.name }}</strong>
<small>{{ account.slug }}</small>
</div>
<form method="post" action="{{ url_for('admin.delete_account', account_id=account.id) }}">
<button class="ghost-button danger-button" type="submit">Endgültig löschen</button>
</form>
</div>
{% endfor %}
{% for category in inactive_categories %}
<div class="month-row">
<div>
<strong>Kategorie: {{ category.name }}</strong>
<small>{{ category.account.name }} · {{ category.slug }}</small>
</div>
<form method="post" action="{{ url_for('admin.delete_category', category_id=category.id) }}">
<button class="ghost-button danger-button" type="submit">Endgültig löschen</button>
</form>
</div>
{% endfor %}
{% for entry in inactive_entries %}
<div class="month-row">
<div>
<strong>Eintrag: {{ entry.name }}</strong>
<small>{{ entry.category.name }} · {{ entry.slug }}</small>
</div>
<form method="post" action="{{ url_for('admin.delete_entry', entry_id=entry.id) }}">
<button class="ghost-button danger-button" type="submit">Endgültig löschen</button>
</form>
</div>
{% endfor %}
{% else %}
<div class="empty-state">Aktuell gibt es keine inaktiven Konten, Kategorien oder Einträge.</div>
{% endif %}
</section>
{% endblock %}