Compare commits
8 Commits
5b752ab7c4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cd85cc5ae | |||
| 487912a1d6 | |||
| 8fd91f1857 | |||
| 53427e0b4d | |||
| 6ba568ea68 | |||
| d5a2721c5f | |||
| 1667eb5d26 | |||
| 26bfa7fb64 |
+119
-22
@@ -177,6 +177,13 @@ def _community_account_totals(month, previous_month, community_accounts, budget_
|
||||
return cards
|
||||
|
||||
|
||||
def _resolve_distribution_direct_entry(entry_rows: list[dict]) -> dict | None:
|
||||
return next(
|
||||
(entry_row for entry_row in entry_rows if entry_row["entry"].is_allocation_target),
|
||||
entry_rows[0] if entry_rows else None,
|
||||
)
|
||||
|
||||
|
||||
def _distribution_hint_map(allocation_service, month, summary, allocations_by_slug, suggestions_by_slug) -> dict[str, dict]:
|
||||
result = {}
|
||||
return {
|
||||
@@ -340,7 +347,9 @@ def detail(label: str):
|
||||
"distribution_kind": "single" if account.slug in {"sparen", "urlaub", "freizeit"} else None,
|
||||
"distribution_suggestion_total": suggestion_total,
|
||||
"distribution_hint": distribution_hints.get(account.slug),
|
||||
"direct_entry": next(
|
||||
"direct_entry": _resolve_distribution_direct_entry(entry_rows)
|
||||
if account.slug in {"sparen", "urlaub", "freizeit"}
|
||||
else next(
|
||||
(entry_row for entry_row in entry_rows if entry_row["entry"].is_allocation_target),
|
||||
None,
|
||||
),
|
||||
@@ -348,6 +357,8 @@ def detail(label: str):
|
||||
}
|
||||
)
|
||||
if account.slug in {"sparen", "urlaub", "freizeit"}:
|
||||
flattened_entries = [entry for category_card in category_cards for entry in category_card["entries"]]
|
||||
direct_entry = _resolve_distribution_direct_entry(flattened_entries)
|
||||
distribution_bucket["categories"].append(
|
||||
{
|
||||
"category": type(
|
||||
@@ -355,7 +366,7 @@ def detail(label: str):
|
||||
(),
|
||||
{"id": account.id * -100, "name": account.name, "account_id": account.id},
|
||||
)(),
|
||||
"entries": [entry for category_card in category_cards for entry in category_card["entries"]],
|
||||
"entries": flattened_entries,
|
||||
"total": account_total,
|
||||
"entry_count": sum(category_card["entry_count"] for category_card in category_cards),
|
||||
"dialog_id": f"distribution-dialog-{account.slug}",
|
||||
@@ -363,15 +374,8 @@ def detail(label: str):
|
||||
"distribution_suggestion_total": distribution_hints.get(account.slug, {}).get("remaining_amount", Decimal("0.00")),
|
||||
"distribution_hint": distribution_hints.get(account.slug),
|
||||
"distribution_account_slug": account.slug,
|
||||
"direct_entry": next(
|
||||
(
|
||||
category_card.get("direct_entry")
|
||||
for category_card in category_cards
|
||||
if category_card.get("direct_entry") is not None
|
||||
),
|
||||
None,
|
||||
),
|
||||
"allow_new_entries": False,
|
||||
"direct_entry": direct_entry,
|
||||
"allow_new_entries": direct_entry is None,
|
||||
}
|
||||
)
|
||||
distribution_bucket["total"] += account_total
|
||||
@@ -681,10 +685,24 @@ def create_category(label: str):
|
||||
flash("Bitte einen Bereich für die Kategorie wählen.", "danger")
|
||||
return _dialog_redirect(month.label, request.form.get("return_dialog") or None)
|
||||
account_id = account.id
|
||||
existing = Category.query.filter_by(account_id=account_id, slug=slugify(name)).first()
|
||||
slug = slugify(name)
|
||||
existing = Category.query.filter_by(account_id=account_id, slug=slug).first()
|
||||
if existing is not None:
|
||||
flash("Kategorie existiert bereits.", "info")
|
||||
return redirect(url_for("planning.detail", label=label))
|
||||
if existing.is_active:
|
||||
flash("Kategorie existiert bereits.", "info")
|
||||
return redirect(url_for("planning.detail", label=label))
|
||||
existing.name = name
|
||||
existing.slug = slug
|
||||
existing.description = request.form.get("description", "").strip() or None
|
||||
existing.community_account_id = (
|
||||
int(request.form["community_account_id"])
|
||||
if account.slug == "gemeinschaftskonto" and request.form.get("community_account_id")
|
||||
else None
|
||||
)
|
||||
existing.is_active = True
|
||||
db.session.commit()
|
||||
flash("Kategorie wiederhergestellt.", "success")
|
||||
return _dialog_redirect(month.label, _category_dialog_id(existing))
|
||||
last_sort = (
|
||||
db.session.scalar(
|
||||
select(Category.sort_order)
|
||||
@@ -702,7 +720,7 @@ def create_category(label: str):
|
||||
else None
|
||||
),
|
||||
name=name,
|
||||
slug=slugify(name),
|
||||
slug=slug,
|
||||
is_active=True,
|
||||
sort_order=last_sort + 1,
|
||||
)
|
||||
@@ -747,7 +765,7 @@ def create_entry(label: str):
|
||||
category_id = request.form.get("category_id")
|
||||
category = None
|
||||
if category_id:
|
||||
category = Category.query.get_or_404(int(category_id))
|
||||
category = Category.query.get_or_404(int(category_id))
|
||||
elif category_name and account_id:
|
||||
category = Category.query.filter_by(
|
||||
account_id=account_id,
|
||||
@@ -772,14 +790,82 @@ def create_entry(label: str):
|
||||
)
|
||||
db.session.add(category)
|
||||
db.session.flush()
|
||||
elif not category.is_active:
|
||||
category.is_active = True
|
||||
db.session.flush()
|
||||
if category is None:
|
||||
flash("Bitte Konto und Kategorie angeben.", "danger")
|
||||
return redirect(url_for("planning.detail", label=label))
|
||||
slug = slugify(request.form.get("slug", "") or name)
|
||||
existing = Entry.query.filter_by(category_id=category.id, slug=slug).first()
|
||||
if existing is not None:
|
||||
flash("Eintrag existiert bereits.", "info")
|
||||
return redirect(url_for("planning.detail", label=label))
|
||||
if existing.is_active:
|
||||
flash("Eintrag existiert bereits.", "info")
|
||||
return redirect(url_for("planning.detail", label=label))
|
||||
existing.name = name
|
||||
existing.slug = slug
|
||||
existing.description = request.form.get("description", "").strip() or None
|
||||
existing.default_amount = to_decimal(request.form.get("default_amount", "0"))
|
||||
existing.amount_type = request.form.get("amount_type", "fixed")
|
||||
existing.benefit_scope = request.form.get("benefit_scope", "all-users")
|
||||
existing.is_allocation_target = request.form.get("is_allocation_target") == "on"
|
||||
existing.is_active = True
|
||||
entry = existing
|
||||
db.session.flush()
|
||||
months = Month.query.order_by(Month.year.asc(), Month.month.asc()).all()
|
||||
current_note = request.form.get("note", "").strip() or None
|
||||
current_amount = _resolve_monthly_amount(request.form)
|
||||
existing_values = {item.month_id: item for item in entry.monthly_values}
|
||||
for target_month in months:
|
||||
value = existing_values.get(target_month.id)
|
||||
if value is None:
|
||||
db.session.add(
|
||||
MonthlyEntryValue(
|
||||
month_id=target_month.id,
|
||||
entry_id=entry.id,
|
||||
planned_amount=current_amount if target_month.id == month.id else entry.default_amount,
|
||||
note=current_note if target_month.id == month.id else None,
|
||||
created_by=current_user.id,
|
||||
updated_by=current_user.id,
|
||||
)
|
||||
)
|
||||
elif target_month.id == month.id:
|
||||
value.planned_amount = current_amount
|
||||
value.note = current_note
|
||||
value.updated_by = current_user.id
|
||||
selected_participants = {
|
||||
int(item) for item in request.form.getlist("participant_ids") if item.strip()
|
||||
}
|
||||
existing_rules = {rule.participant_id: rule for rule in entry.share_rules}
|
||||
for participant_id, rule in list(existing_rules.items()):
|
||||
if participant_id not in selected_participants:
|
||||
db.session.delete(rule)
|
||||
for participant_id in selected_participants:
|
||||
rule = existing_rules.get(participant_id)
|
||||
if rule is None:
|
||||
db.session.add(
|
||||
EntryShareRule(
|
||||
entry_id=entry.id,
|
||||
participant_id=participant_id,
|
||||
share_type="equal",
|
||||
)
|
||||
)
|
||||
else:
|
||||
rule.share_type = "equal"
|
||||
rule.share_value = None
|
||||
current_app.extensions["saldo.month_service"].sync_distribution_allocation_from_entry(
|
||||
month,
|
||||
entry,
|
||||
mark_manual=True,
|
||||
)
|
||||
db.session.commit()
|
||||
current_app.extensions["saldo.month_service"].refresh_suggestions(
|
||||
month, reason="Eintrag wurde wiederhergestellt"
|
||||
)
|
||||
db.session.commit()
|
||||
flash("Eintrag wiederhergestellt.", "success")
|
||||
return_dialog = request.form.get("return_dialog") or _category_dialog_id(category)
|
||||
return _dialog_redirect(label, return_dialog)
|
||||
last_sort = (
|
||||
db.session.scalar(
|
||||
select(Entry.sort_order)
|
||||
@@ -872,10 +958,21 @@ def create_community_account(label: str):
|
||||
if not name:
|
||||
flash("Bitte einen Namen für das Gemeinschaftskonto angeben.", "danger")
|
||||
return _dialog_redirect(label, "community-account-create-dialog")
|
||||
existing = CommunityAccount.query.filter_by(slug=slugify(name)).first()
|
||||
slug = slugify(name)
|
||||
existing = CommunityAccount.query.filter_by(slug=slug).first()
|
||||
if existing is not None:
|
||||
flash("Gemeinschaftskonto existiert bereits.", "info")
|
||||
return _dialog_redirect(label, "community-account-create-dialog")
|
||||
if existing.is_active:
|
||||
flash("Gemeinschaftskonto existiert bereits.", "info")
|
||||
return _dialog_redirect(label, "community-account-create-dialog")
|
||||
existing.name = name
|
||||
existing.slug = slug
|
||||
existing.account_type = "shared"
|
||||
existing.linked_account_slug = None
|
||||
existing.description = request.form.get("description", "").strip() or None
|
||||
existing.is_active = True
|
||||
db.session.commit()
|
||||
flash("Gemeinschaftskonto wiederhergestellt.", "success")
|
||||
return _dialog_redirect(label)
|
||||
last_sort = (
|
||||
db.session.scalar(
|
||||
select(CommunityAccount.sort_order)
|
||||
@@ -887,7 +984,7 @@ def create_community_account(label: str):
|
||||
)
|
||||
community_account = CommunityAccount(
|
||||
name=name,
|
||||
slug=slugify(name),
|
||||
slug=slug,
|
||||
account_type="shared",
|
||||
description=request.form.get("description", "").strip() or None,
|
||||
sort_order=last_sort + 1,
|
||||
|
||||
+46
-11
@@ -13,6 +13,7 @@ from app.models import (
|
||||
MonthlyIncome,
|
||||
NotificationPreference,
|
||||
User,
|
||||
to_decimal,
|
||||
)
|
||||
|
||||
|
||||
@@ -203,6 +204,44 @@ def _deactivate_placeholder_categories() -> None:
|
||||
category.community_account_id = None
|
||||
|
||||
|
||||
def _restore_existing_standard_structure() -> None:
|
||||
for account_slug, account_data in ACCOUNT_TREE.items():
|
||||
account = Account.query.filter_by(slug=account_slug).first()
|
||||
if account is None:
|
||||
continue
|
||||
for category_slug, entries in account_data["categories"].items():
|
||||
category = Category.query.filter_by(account_id=account.id, slug=category_slug).first()
|
||||
legacy_slug = LEGACY_CATEGORY_SLUGS.get((account_slug, category_slug))
|
||||
if category is None and legacy_slug:
|
||||
category = Category.query.filter_by(account_id=account.id, slug=legacy_slug).first()
|
||||
if category is None:
|
||||
continue
|
||||
category.is_active = True
|
||||
for entry_name in entries:
|
||||
entry_slug = slugify(entry_name)
|
||||
entry = Entry.query.filter_by(category_id=category.id, slug=entry_slug).first()
|
||||
legacy_entry_name = LEGACY_ENTRY_NAMES.get((account_slug, category_slug, entry_name))
|
||||
if entry is None and legacy_entry_name:
|
||||
entry = Entry.query.filter_by(
|
||||
category_id=category.id,
|
||||
slug=slugify(legacy_entry_name),
|
||||
).first()
|
||||
if entry is not None:
|
||||
entry.is_active = True
|
||||
|
||||
|
||||
def _restore_existing_budget_visibility() -> None:
|
||||
gemeinschaft = Account.query.filter_by(slug="gemeinschaftskonto").first()
|
||||
if gemeinschaft is None:
|
||||
return
|
||||
for category in gemeinschaft.categories:
|
||||
if category.is_active:
|
||||
continue
|
||||
active_entries = [entry for entry in category.entries if entry.is_active]
|
||||
if active_entries:
|
||||
category.is_active = True
|
||||
|
||||
|
||||
def seed_data(include_example_entries: bool = False) -> None:
|
||||
# Basisdaten nur für die fachliche Grundstruktur, ohne Demo-Benutzer,
|
||||
# Beispiel-Personen oder vorausgefüllte Monatsdaten.
|
||||
@@ -392,18 +431,14 @@ def seed_data(include_example_entries: bool = False) -> None:
|
||||
"Person 1",
|
||||
"Person 2",
|
||||
}
|
||||
for category in gemeinschaft.categories:
|
||||
if category.slug not in ACCOUNT_TREE["gemeinschaftskonto"]["categories"]:
|
||||
category.is_active = False
|
||||
elif category.community_account_id is None:
|
||||
category.community_account_id = community_accounts["hauptkonto"].id
|
||||
else:
|
||||
_deactivate_placeholder_categories()
|
||||
_deactivate_placeholder_entries()
|
||||
for category in gemeinschaft.categories:
|
||||
if category.slug not in ACCOUNT_TREE["gemeinschaftskonto"]["categories"]:
|
||||
category.is_active = False
|
||||
elif category.community_account_id is None:
|
||||
category.community_account_id = community_accounts["hauptkonto"].id
|
||||
|
||||
if not include_example_entries:
|
||||
_deactivate_placeholder_categories()
|
||||
_deactivate_placeholder_entries()
|
||||
_restore_existing_standard_structure()
|
||||
_restore_existing_budget_visibility()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
+48
-2
@@ -717,11 +717,15 @@ h2 { font-size: 1.2rem; margin-bottom: 0; }
|
||||
|
||||
.bottom-nav {
|
||||
position: fixed;
|
||||
inset: auto 12px 12px 12px;
|
||||
left: max(8px, env(safe-area-inset-left));
|
||||
right: max(8px, env(safe-area-inset-right));
|
||||
bottom: calc(8px + env(safe-area-inset-bottom));
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
max-width: 540px;
|
||||
margin: 0 auto;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 24px;
|
||||
@@ -733,10 +737,18 @@ h2 { font-size: 1.2rem; margin-bottom: 0; }
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.1;
|
||||
padding: 10px 6px;
|
||||
border-radius: 16px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.bottom-nav a span {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.mobile-theme-toggle {
|
||||
@@ -784,6 +796,40 @@ h2 { font-size: 1.2rem; margin-bottom: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.content {
|
||||
padding: 18px 14px 104px;
|
||||
}
|
||||
|
||||
.bottom-nav {
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.bottom-nav a {
|
||||
gap: 4px;
|
||||
font-size: 0.56rem;
|
||||
line-height: 1.05;
|
||||
padding: 7px 2px;
|
||||
}
|
||||
|
||||
.bottom-nav a span {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
min-height: 2.2em;
|
||||
text-wrap: balance;
|
||||
overflow-wrap: normal;
|
||||
word-break: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.bottom-nav a .ui-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.planning-hero-strong {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
@@ -76,14 +76,14 @@
|
||||
</div>
|
||||
{% if current_user.is_authenticated %}
|
||||
<nav class="bottom-nav">
|
||||
<a href="{{ url_for('main.index') }}"><img src="{{ url_for('static', filename='icons/house.svg') }}" alt="" class="ui-icon">Übersicht</a>
|
||||
<a href="{{ url_for('planning.current') }}"><img src="{{ url_for('static', filename='icons/wallet.svg') }}" alt="" class="ui-icon">Planung</a>
|
||||
<a href="{{ url_for('months.index') }}"><img src="{{ url_for('static', filename='icons/calendar.svg') }}" alt="" class="ui-icon">Monate</a>
|
||||
<a href="{{ url_for('main.analytics') }}"><img src="{{ url_for('static', filename='icons/chart-simple.svg') }}" alt="" class="ui-icon">Auswertungen</a>
|
||||
<a href="{{ url_for('main.index') }}" aria-label="Übersicht" title="Übersicht"><img src="{{ url_for('static', filename='icons/house.svg') }}" alt="" class="ui-icon"><span>Übersicht</span></a>
|
||||
<a href="{{ url_for('planning.current') }}" aria-label="Planung" title="Planung"><img src="{{ url_for('static', filename='icons/wallet.svg') }}" alt="" class="ui-icon"><span>Planung</span></a>
|
||||
<a href="{{ url_for('months.index') }}" aria-label="Monate" title="Monate"><img src="{{ url_for('static', filename='icons/calendar.svg') }}" alt="" class="ui-icon"><span>Monate</span></a>
|
||||
<a href="{{ url_for('main.analytics') }}" aria-label="Auswertungen" title="Auswertungen"><img src="{{ url_for('static', filename='icons/chart-simple.svg') }}" alt="" class="ui-icon"><span>Auswertungen</span></a>
|
||||
{% if current_user.is_admin() %}
|
||||
<a href="{{ url_for('admin.index') }}"><img src="{{ url_for('static', filename='icons/sliders.svg') }}" alt="" class="ui-icon">Optionen</a>
|
||||
<a href="{{ url_for('admin.index') }}" aria-label="Optionen" title="Optionen"><img src="{{ url_for('static', filename='icons/sliders.svg') }}" alt="" class="ui-icon"><span>Optionen</span></a>
|
||||
{% else %}
|
||||
<a href="#" data-enable-push><img src="{{ url_for('static', filename='icons/bell.svg') }}" alt="" class="ui-icon">Push</a>
|
||||
<a href="#" data-enable-push aria-label="Push" title="Push"><img src="{{ url_for('static', filename='icons/bell.svg') }}" alt="" class="ui-icon"><span>Push</span></a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
@@ -226,10 +226,25 @@
|
||||
<div class="dialog-action-row dialog-action-spread">
|
||||
<button class="ghost-button small-button" type="submit">Speichern</button>
|
||||
{% if month.incomes|length > 1 %}
|
||||
<button class="ghost-button danger-button small-button" type="submit" formaction="{{ url_for('planning.delete_income', label=month.label, income_id=income.id) }}">Löschen</button>
|
||||
<button class="ghost-button danger-button small-button" type="button" data-open-dialog="confirm-delete-income-{{ income.id }}">Löschen</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% if month.incomes|length > 1 %}
|
||||
<dialog id="confirm-delete-income-{{ income.id }}" class="app-dialog confirm-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<h3>Einkommenszeile wirklich löschen?</h3>
|
||||
<p class="muted">`{{ income.label }}` wird aus diesem Monat entfernt.</p>
|
||||
<form method="post" action="{{ url_for('planning.delete_income', label=month.label, income_id=income.id) }}" class="dialog-action-row dialog-action-spread">
|
||||
<button class="ghost-button" type="button" data-open-dialog="income-dialog">Zurück</button>
|
||||
<button class="primary-button danger-fill-button" type="submit">Jetzt löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('planning.create_income', label=month.label) }}" class="soft-form-section stack-form">
|
||||
@@ -455,7 +470,7 @@
|
||||
<small>{{ category_data.total|currency }} · {{ category_data.entry_count }} Einträge</small>
|
||||
</div>
|
||||
{% if category_data.allow_new_entries %}
|
||||
<button class="ghost-button icon-button" type="button" data-open-dialog="dialog-add-entry" data-account-id="{{ category_data.category.account_id }}" data-category-name="{{ category_data.category.name }}" data-return-dialog="{{ category_data.dialog_id }}" data-area="{{ 'budget' if category_data.category.account.slug == 'gemeinschaftskonto' else 'distribution' }}">
|
||||
<button class="ghost-button icon-button" type="button" data-open-dialog="dialog-add-entry" data-account-id="{{ category_data.category.account_id }}" data-category-name="{{ category_data.category.name }}" data-return-dialog="{{ category_data.dialog_id }}" data-area="{{ 'budget' if account_data.account.slug == 'gemeinschaftskonto' else 'distribution' }}">
|
||||
<img src="{{ url_for('static', filename='icons/plus.svg') }}" alt="" class="ui-icon small-ui-icon">
|
||||
Eintrag
|
||||
</button>
|
||||
@@ -598,8 +613,20 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<form method="post" action="{{ url_for('planning.delete_category', label=month.label, category_id=category_data.category.id) }}">
|
||||
<button class="ghost-button danger-button" type="submit">Kategorie löschen</button>
|
||||
<button class="ghost-button danger-button" type="button" data-open-dialog="confirm-delete-category-{{ category_data.category.id }}">Kategorie löschen</button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="confirm-delete-category-{{ category_data.category.id }}" class="app-dialog confirm-dialog">
|
||||
<form method="dialog" class="dialog-close-row">
|
||||
<button class="dialog-close-button" type="submit" aria-label="Schließen">×</button>
|
||||
</form>
|
||||
<div class="stack-form">
|
||||
<h3>Kategorie wirklich löschen?</h3>
|
||||
<p class="muted">`{{ category_data.category.name }}` wird ausgeblendet und ihre Einträge werden mit ausgeblendet.</p>
|
||||
<form method="post" action="{{ url_for('planning.delete_category', label=month.label, category_id=category_data.category.id) }}" class="dialog-action-row dialog-action-spread">
|
||||
<button class="ghost-button" type="button" data-open-dialog="{{ category_data.dialog_id }}">Zurück</button>
|
||||
<button class="primary-button danger-fill-button" type="submit">Jetzt löschen</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -101,6 +101,19 @@ def test_distribution_dialog_shows_direct_budget_form(logged_in_client):
|
||||
assert b"Sparkonto" in response.data
|
||||
|
||||
|
||||
def test_distribution_dialog_keeps_direct_budget_form_when_target_flag_is_missing(logged_in_client):
|
||||
month = Month.query.filter_by(label="2026-04").first()
|
||||
sparen_value = next(item for item in month.entry_values if item.entry.name == "Sparziel")
|
||||
sparen_value.entry.is_allocation_target = False
|
||||
db.session.commit()
|
||||
|
||||
response = logged_in_client.get("/planning/2026-04")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"Budget direkt anpassen" in response.data
|
||||
assert b"Monatliches Budget" in response.data
|
||||
|
||||
|
||||
def test_planning_shows_budgets_and_community_accounts(logged_in_client):
|
||||
response = logged_in_client.get("/planning/2026-04")
|
||||
|
||||
@@ -111,6 +124,17 @@ def test_planning_shows_budgets_and_community_accounts(logged_in_client):
|
||||
assert b"Auszahlung Person B" in response.data
|
||||
|
||||
|
||||
def test_planning_delete_actions_render_app_confirmation_dialogs(logged_in_client):
|
||||
response = logged_in_client.get("/planning/2026-04")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b"confirm-delete-income-" in response.data
|
||||
assert b"confirm-delete-category-" in response.data
|
||||
assert b"confirm-delete-community-account-" in response.data
|
||||
assert b"confirm-delete-entry-" in response.data
|
||||
assert b"data-confirm-submit" not in response.data
|
||||
|
||||
|
||||
def test_community_account_can_assign_budget_categories(logged_in_client):
|
||||
community_account = CommunityAccount.query.filter_by(slug="hauptkonto").first()
|
||||
category = Category.query.filter_by(slug="wohnen").first()
|
||||
@@ -130,6 +154,59 @@ def test_community_account_can_assign_budget_categories(logged_in_client):
|
||||
assert category.community_account_id == community_account.id
|
||||
|
||||
|
||||
def test_deleted_category_can_be_created_again(logged_in_client):
|
||||
create_response = logged_in_client.post(
|
||||
"/planning/2026-04/categories",
|
||||
data={"name": "Testbudget", "area": "budget"},
|
||||
)
|
||||
assert create_response.status_code == 302
|
||||
|
||||
category = Category.query.filter_by(slug="testbudget").first()
|
||||
original_id = category.id
|
||||
|
||||
delete_response = logged_in_client.post(f"/planning/2026-04/categories/{category.id}/delete")
|
||||
assert delete_response.status_code == 302
|
||||
|
||||
recreate_response = logged_in_client.post(
|
||||
"/planning/2026-04/categories",
|
||||
data={"name": "Testbudget", "area": "budget"},
|
||||
)
|
||||
|
||||
restored = Category.query.filter_by(slug="testbudget").first()
|
||||
|
||||
assert recreate_response.status_code == 302
|
||||
assert restored.id == original_id
|
||||
assert restored.is_active is True
|
||||
|
||||
|
||||
def test_deleted_community_account_can_be_created_again(logged_in_client):
|
||||
create_response = logged_in_client.post(
|
||||
"/planning/2026-04/community-accounts",
|
||||
data={"name": "Testkonto", "description": ""},
|
||||
)
|
||||
assert create_response.status_code == 302
|
||||
|
||||
community_account = CommunityAccount.query.filter_by(slug="testkonto").first()
|
||||
original_id = community_account.id
|
||||
|
||||
delete_response = logged_in_client.post(
|
||||
f"/planning/2026-04/community-accounts/{community_account.id}/delete"
|
||||
)
|
||||
assert delete_response.status_code == 302
|
||||
|
||||
recreate_response = logged_in_client.post(
|
||||
"/planning/2026-04/community-accounts",
|
||||
data={"name": "Testkonto", "description": "Wieder da"},
|
||||
)
|
||||
|
||||
restored = CommunityAccount.query.filter_by(slug="testkonto").first()
|
||||
|
||||
assert recreate_response.status_code == 302
|
||||
assert restored.id == original_id
|
||||
assert restored.is_active is True
|
||||
assert restored.description == "Wieder da"
|
||||
|
||||
|
||||
def test_community_account_rejects_budget_assigned_to_other_account(logged_in_client):
|
||||
primary_account = CommunityAccount.query.filter_by(slug="hauptkonto").first()
|
||||
secondary_response = logged_in_client.post(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.extensions import db
|
||||
from app.models import Account, Category, Entry
|
||||
from app.seed import seed_data
|
||||
|
||||
|
||||
def test_seed_restores_hidden_budget_categories_with_active_entries(app):
|
||||
budget_account = Account.query.filter_by(slug="gemeinschaftskonto").first()
|
||||
category = Category(
|
||||
account_id=budget_account.id,
|
||||
name="Legacy Budget",
|
||||
slug="legacy-budget",
|
||||
is_active=False,
|
||||
sort_order=999,
|
||||
)
|
||||
db.session.add(category)
|
||||
db.session.flush()
|
||||
db.session.add(
|
||||
Entry(
|
||||
category_id=category.id,
|
||||
name="Legacy Eintrag",
|
||||
slug="legacy-eintrag",
|
||||
default_amount=0,
|
||||
amount_type="fixed",
|
||||
benefit_scope="all-users",
|
||||
is_active=True,
|
||||
sort_order=1,
|
||||
)
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
seed_data()
|
||||
db.session.expire_all()
|
||||
|
||||
restored = Category.query.filter_by(account_id=budget_account.id, slug="legacy-budget").first()
|
||||
assert restored is not None
|
||||
assert restored.is_active is True
|
||||
Reference in New Issue
Block a user