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 <noreply@anthropic.com>
This commit is contained in:
parent
8c0225a297
commit
e2b756bcc7
@ -391,6 +391,9 @@ class Goal(models.Model):
|
|||||||
if self.kind == self.KIND_SAVING:
|
if self.kind == self.KIND_SAVING:
|
||||||
# Provisional: saldo de la cuenta asociada. Cuando exista el
|
# Provisional: saldo de la cuenta asociada. Cuando exista el
|
||||||
# módulo de inversiones, esta rama es lo único que hay que tocar.
|
# 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")
|
return self.account.current_balance() if self.account else Decimal("0")
|
||||||
|
|
||||||
if not self.category:
|
if not self.category:
|
||||||
@ -401,9 +404,11 @@ class Goal(models.Model):
|
|||||||
else:
|
else:
|
||||||
category_ids = [self.category_id]
|
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(
|
expenses = Expense.objects.filter(
|
||||||
owner=self.owner, category_id__in=category_ids
|
owner=self.owner, category_id__in=category_ids
|
||||||
)
|
).exclude(is_transfer=True)
|
||||||
|
|
||||||
start = self._period_start()
|
start = self._period_start()
|
||||||
if start:
|
if start:
|
||||||
|
|||||||
@ -96,7 +96,7 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<strong>Total:</strong> {{ kpi_total|floatformat:2 }}€ |
|
<strong>Total (sin traspasos):</strong> {{ kpi_total|floatformat:2 }}€ |
|
||||||
<strong>Gastos:</strong> {{ kpi_count }} |
|
<strong>Gastos:</strong> {{ kpi_count }} |
|
||||||
<strong>Categorías:</strong> {{ kpi_categories }}
|
<strong>Categorías:</strong> {{ kpi_categories }}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -77,9 +77,26 @@ def _category_tree(categories):
|
|||||||
return rows
|
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):
|
def _account_balances(accounts):
|
||||||
"""Calcula el saldo de cada cuenta con dos queries agregadas (nada de
|
"""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]
|
account_ids = [a.id for a in accounts]
|
||||||
|
|
||||||
expense_totals = {
|
expense_totals = {
|
||||||
@ -176,7 +193,9 @@ def _build_account_charts(accounts, balance_by_account, selected_year, today):
|
|||||||
@login_required
|
@login_required
|
||||||
def home(request):
|
def home(request):
|
||||||
today = date.today()
|
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 ----
|
# ---- KPIs del mes en curso ----
|
||||||
month_expenses = expenses.filter(date__year=today.year, date__month=today.month)
|
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)
|
has_alerts = bool(exceeded_goals or negative_accounts)
|
||||||
|
|
||||||
# ---- Últimos movimientos (gastos + ingresos mezclados) ----
|
# ---- Últimos movimientos (gastos + ingresos mezclados) ----
|
||||||
recent_expenses = expenses.select_related("category", "account").order_by(
|
# Es un listado, no un análisis: los traspasos son movimientos reales de las
|
||||||
"-date", "-id"
|
# cuentas y salen marcados con un distintivo, así que no se cuelga de
|
||||||
)[:8]
|
# "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 = (
|
recent_incomes = (
|
||||||
Income.objects.filter(owner=request.user)
|
Income.objects.filter(owner=request.user)
|
||||||
.select_related("account")
|
.select_related("account")
|
||||||
@ -329,11 +353,16 @@ def expense_list(request):
|
|||||||
|
|
||||||
expenses = expenses.order_by("-date")
|
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
|
# Pagination
|
||||||
paginator = Paginator(expenses, 10)
|
paginator = Paginator(expenses, 10)
|
||||||
@ -490,7 +519,9 @@ def dashboard(request):
|
|||||||
kpi_balance = total_balance
|
kpi_balance = total_balance
|
||||||
|
|
||||||
# Filter by base expenses
|
# 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:
|
if account_id:
|
||||||
expenses = expenses.filter(account_id=account_id)
|
expenses = expenses.filter(account_id=account_id)
|
||||||
|
|
||||||
@ -576,7 +607,7 @@ def dashboard(request):
|
|||||||
category_comparison = []
|
category_comparison = []
|
||||||
|
|
||||||
if compare_enabled:
|
if compare_enabled:
|
||||||
previous_expenses = Expense.objects.filter(owner=request.user)
|
previous_expenses = _analysis_expenses(request.user)
|
||||||
if account_id:
|
if account_id:
|
||||||
previous_expenses = previous_expenses.filter(account_id=account_id)
|
previous_expenses = previous_expenses.filter(account_id=account_id)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user