Modificaciones en filtrado y creada opción para marcar que un gasto/ingreso es un traspaso de cuentas #34

Merged
jkuijperm merged 18 commits from dev into main 2026-09-16 07:50:18 +00:00
3 changed files with 48 additions and 12 deletions
Showing only changes of commit e2b756bcc7 - Show all commits

View File

@ -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:

View File

@ -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>

View File

@ -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 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)