from __future__ import annotations from decimal import Decimal, ROUND_HALF_UP from flask import ( Blueprint, current_app, flash, g, jsonify, redirect, render_template, request, url_for, ) from flask_login import current_user, login_required from sqlalchemy import select from app.extensions import db from app.models import ( Account, Category, CommunityAccount, CostParticipant, Entry, EntryShareRule, Month, MonthlyAllocation, MonthlyEntryValue, MonthlyIncome, PushSubscription, to_decimal, ) from app.seed import slugify from app.utils.uploads import save_avatar_upload from app.utils.users import ( active_users, benefit_scope_label, benefit_scope_options, decode_benefit_scope, format_planning_month, personal_account_names, sync_user_participants, ) planning_bp = Blueprint("planning", __name__, url_prefix="/planning") def _dialog_redirect(label: str, dialog_id: str | None = None): if dialog_id: return redirect(url_for("planning.detail", label=label, dialog=dialog_id)) return redirect(url_for("planning.detail", label=label)) def _category_dialog_id(category: Category | None) -> str | None: if category is None or category.account is None: return None if category.account.slug in {"sparen", "urlaub", "freizeit"}: return f"distribution-dialog-{category.account.slug}" if category.account.slug in {"persoenlich-flo", "persoenlich-desi"}: return "personal-split-dialog" return f"category-dialog-{category.id}" def _account_from_form(area_key: str = "area") -> Account | None: account_id = request.form.get("account_id") if account_id: return Account.query.get(int(account_id)) area = (request.form.get(area_key) or "").strip() if area == "budget": return Account.query.filter_by(slug="gemeinschaftskonto").first() if area == "distribution": return Account.query.filter_by(slug="sparen").first() return None def _resolve_monthly_amount(form, monthly_key: str = "planned_amount", annual_key: str = "annual_amount") -> Decimal: monthly_raw = (form.get(monthly_key) or "").strip() annual_raw = (form.get(annual_key) or "").strip() if monthly_raw: return to_decimal(monthly_raw) if annual_raw: return (to_decimal(annual_raw) / Decimal("12.00")).quantize( Decimal("0.01"), rounding=ROUND_HALF_UP ) return Decimal("0.00") def _resolve_avatar_url(existing: str | None = None) -> str | None: upload = request.files.get("avatar_file") if upload and upload.filename: try: return save_avatar_upload(upload) except ValueError as exc: flash(str(exc), "danger") return existing avatar_url = request.form.get("avatar_url") if avatar_url is not None: avatar_url = avatar_url.strip() return avatar_url or existing return existing def _community_account_totals(month, previous_month, community_accounts, budget_categories): current_entry_values = {item.entry_id: to_decimal(item.planned_amount) for item in month.entry_values} previous_entry_values = ( {item.entry_id: to_decimal(item.planned_amount) for item in previous_month.entry_values} if previous_month is not None else {} ) previous_allocations = ( { item.target_account.slug: to_decimal(item.amount) for item in previous_month.allocations if item.target_account } if previous_month is not None else {} ) cards = [] for community_account in community_accounts: if community_account.account_type == "personal" and community_account.linked_account_slug: current_total = next( ( to_decimal(item.amount) for item in month.allocations if item.target_account and item.target_account.slug == community_account.linked_account_slug ), Decimal("0.00"), ) previous_total = previous_allocations.get(community_account.linked_account_slug, Decimal("0.00")) assigned_budget_names = ["Persönliche Auszahlung"] else: assigned_categories = [ category for category in budget_categories if category.community_account_id == community_account.id ] current_total = sum( ( current_entry_values.get(entry.id, Decimal("0.00")) for category in assigned_categories for entry in category.entries if entry.is_active ), Decimal("0.00"), ) previous_total = sum( ( previous_entry_values.get(entry.id, Decimal("0.00")) for category in assigned_categories for entry in category.entries if entry.is_active ), Decimal("0.00"), ) assigned_budget_names = [category.name for category in assigned_categories] delta = current_total - previous_total cards.append( { "community_account": community_account, "current_total": current_total, "previous_total": previous_total, "delta": delta, "assigned_budget_names": assigned_budget_names, "is_read_only": community_account.account_type == "personal", } ) return cards def _distribution_hint_map(allocation_service, month, summary, allocations_by_slug, suggestions_by_slug) -> dict[str, dict]: result = {} return { slug: { **hint, "current_pct": ( (to_decimal(allocations_by_slug.get(slug).amount) / summary.total_income * Decimal("100.00")).quantize(Decimal("0.01")) if summary.total_income > Decimal("0.00") and allocations_by_slug.get(slug) is not None else Decimal("0.00") ), "status": ( "good" if summary.total_income <= Decimal("0.00") else "bad" if ( allocations_by_slug.get(slug) is not None and (to_decimal(allocations_by_slug.get(slug).amount) / summary.total_income * Decimal("100.00")) < hint["min_pct"] ) else "good" ), "remaining_amount": max( Decimal("0.00"), to_decimal(suggestions_by_slug.get(slug).suggested_amount if suggestions_by_slug.get(slug) else 0) - to_decimal(allocations_by_slug.get(slug).amount if allocations_by_slug.get(slug) else 0), ), "target_amount": to_decimal(suggestions_by_slug.get(slug).suggested_amount if suggestions_by_slug.get(slug) else 0), } for slug in allocation_service.TARGET_SLUGS if (hint := allocation_service.strategy_hint(month, slug)) is not None } @planning_bp.route("/") @login_required def current(): return redirect(url_for("planning.detail", label=g.current_month.label)) @planning_bp.route("/