From e2b756bcc78191fcf67122f55e52126fd71c10f7 Mon Sep 17 00:00:00 2001 From: JKuijperM Date: Wed, 16 Sep 2026 08:30:43 +0200 Subject: [PATCH] Excluye los traspasos de los calculos de analisis Un traspaso mueve dinero entre cuentas propias: suma al saldo de cada cuenta pero no es gasto ni ingreso real. Las agregaciones de analisis (KPIs, graficos de home y dashboard, comparativa de periodos, totales del listado y objetivos de pago y presupuesto) pasan a excluirlo. Hay dos comportamientos opuestos sobre el mismo campo y conviene no confundirlos: - Analisis: excluye los traspasos. De ahi el helper _analysis_expenses, que documenta en su docstring donde NO debe usarse. - Saldo: los incluye. _account_balances, current_balance, monthly_balance, balance_until y monthly_net quedan intactos a proposito; filtrarlos ahi descuadraria todos los saldos de la aplicacion de forma silenciosa, que es mucho peor y menos evidente que inflar un KPI. Por el mismo motivo la rama saving de Goal.progress tampoco filtra: mandar dinero a la cuenta de ahorro es justo como se progresa en ese objetivo. Los ultimos movimientos de home dejan de colgar de la consulta de KPIs y usan la suya propia: son un listado, no un analisis, y un traspaso tiene que poder verse ahi. Lo mismo en el listado de gastos, donde la tabla los sigue mostrando y solo los totales los descuentan. Los managers por defecto de Expense e Income no se tocan. Co-Authored-By: Claude Opus 5 --- expenses_manager/expenses/models.py | 7 ++- .../templates/expenses/expense_list.html | 2 +- expenses_manager/expenses/views.py | 51 +++++++++++++++---- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/expenses_manager/expenses/models.py b/expenses_manager/expenses/models.py index 14ca8d8..97e0e19 100644 --- a/expenses_manager/expenses/models.py +++ b/expenses_manager/expenses/models.py @@ -391,6 +391,9 @@ class Goal(models.Model): if self.kind == self.KIND_SAVING: # Provisional: saldo de la cuenta asociada. Cuando exista el # módulo de inversiones, esta rama es lo único que hay que tocar. + # Ojo: aquí los traspasos SÍ cuentan. Mandar dinero a la cuenta de + # ahorro es precisamente cómo se progresa en un objetivo de ahorro, + # así que no filtres is_transfer en esta rama. return self.account.current_balance() if self.account else Decimal("0") if not self.category: @@ -401,9 +404,11 @@ class Goal(models.Model): else: category_ids = [self.category_id] + # Pago y presupuesto miden gasto real, así que los traspasos entre + # cuentas propias no cuentan. La rama de ahorro de arriba es la opuesta. expenses = Expense.objects.filter( owner=self.owner, category_id__in=category_ids - ) + ).exclude(is_transfer=True) start = self._period_start() if start: diff --git a/expenses_manager/expenses/templates/expenses/expense_list.html b/expenses_manager/expenses/templates/expenses/expense_list.html index 11b6134..10db769 100644 --- a/expenses_manager/expenses/templates/expenses/expense_list.html +++ b/expenses_manager/expenses/templates/expenses/expense_list.html @@ -96,7 +96,7 @@
- Total: {{ kpi_total|floatformat:2 }}€ | + Total (sin traspasos): {{ kpi_total|floatformat:2 }}€ | Gastos: {{ kpi_count }} | Categorías: {{ kpi_categories }}
diff --git a/expenses_manager/expenses/views.py b/expenses_manager/expenses/views.py index 4ad3976..c9864c1 100644 --- a/expenses_manager/expenses/views.py +++ b/expenses_manager/expenses/views.py @@ -77,9 +77,26 @@ def _category_tree(categories): return rows +def _analysis_expenses(user): + """Gastos para análisis: excluye los traspasos entre cuentas propias. + + Un traspaso mueve dinero entre cuentas del usuario, así que suma al saldo de + cada cuenta pero NO es gasto real. Usa este helper en KPIs, gráficos y + comparativas. + + NO lo uses en cálculos de saldo (_account_balances, current_balance, + monthly_balance, balance_until, monthly_net): ahí los traspasos SÍ cuentan, + y filtrarlos descuadraría todos los saldos. + """ + return Expense.objects.filter(owner=user).exclude(is_transfer=True) + + def _account_balances(accounts): """Calcula el saldo de cada cuenta con dos queries agregadas (nada de - account.current_balance() en bucle, que dispararía N+1).""" + account.current_balance() en bucle, que dispararía N+1). + + Los traspasos cuentan aquí a propósito: son movimientos reales de las + cuentas. No metas _analysis_expenses en esta función.""" account_ids = [a.id for a in accounts] expense_totals = { @@ -176,7 +193,9 @@ def _build_account_charts(accounts, balance_by_account, selected_year, today): @login_required def home(request): today = date.today() - expenses = Expense.objects.filter(owner=request.user) + # Base de los KPIs, la comparativa y el mini-gráfico: sin traspasos. + # Los últimos movimientos usan su propia query, que sí los incluye. + expenses = _analysis_expenses(request.user) # ---- KPIs del mes en curso ---- month_expenses = expenses.filter(date__year=today.year, date__month=today.month) @@ -214,9 +233,14 @@ def home(request): has_alerts = bool(exceeded_goals or negative_accounts) # ---- Últimos movimientos (gastos + ingresos mezclados) ---- - recent_expenses = expenses.select_related("category", "account").order_by( - "-date", "-id" - )[:8] + # Es un listado, no un análisis: los traspasos son movimientos reales de las + # cuentas y salen marcados con un distintivo, así que no se cuelga de + # "expenses" (que ya los ha filtrado) sino de la query sin filtrar. + recent_expenses = ( + Expense.objects.filter(owner=request.user) + .select_related("category", "account") + .order_by("-date", "-id")[:8] + ) recent_incomes = ( Income.objects.filter(owner=request.user) .select_related("account") @@ -329,11 +353,16 @@ def expense_list(request): expenses = expenses.order_by("-date") - total_amount = expenses.aggregate(total=Sum("amount"))["total"] or 0 + # La tabla sigue listando los traspasos (hay que poder verlos y corregirlos), + # pero el total y los contadores miden gasto real, así que los excluyen. Por + # eso la etiqueta del total dice "sin traspasos" en la plantilla. + analysis_expenses = expenses.exclude(is_transfer=True) - expense_count = expenses.count() + total_amount = analysis_expenses.aggregate(total=Sum("amount"))["total"] or 0 - category_count = expenses.values("category").distinct().count() + expense_count = analysis_expenses.count() + + category_count = analysis_expenses.values("category").distinct().count() # Pagination paginator = Paginator(expenses, 10) @@ -490,7 +519,9 @@ def dashboard(request): kpi_balance = total_balance # Filter by base expenses - expenses = Expense.objects.filter(owner=request.user) + # Todo lo que cuelga de aquí es análisis (KPIs, gráficos, comparativa), así + # que va sin traspasos. Los saldos de arriba sí los incluyen. + expenses = _analysis_expenses(request.user) if account_id: expenses = expenses.filter(account_id=account_id) @@ -576,7 +607,7 @@ def dashboard(request): category_comparison = [] if compare_enabled: - previous_expenses = Expense.objects.filter(owner=request.user) + previous_expenses = _analysis_expenses(request.user) if account_id: previous_expenses = previous_expenses.filter(account_id=account_id)