From e64421b3df85df2e93f52f7a5eb0748e1153cc66 Mon Sep 17 00:00:00 2001 From: JKuijperM Date: Wed, 21 Oct 2026 18:41:01 +0200 Subject: [PATCH] Add savings account flag and budget block model --- .../0019_account_is_savings_budgetblock.py | 34 +++++++++++++++ expenses_manager/expenses/models.py | 41 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 expenses_manager/expenses/migrations/0019_account_is_savings_budgetblock.py diff --git a/expenses_manager/expenses/migrations/0019_account_is_savings_budgetblock.py b/expenses_manager/expenses/migrations/0019_account_is_savings_budgetblock.py new file mode 100644 index 0000000..b968216 --- /dev/null +++ b/expenses_manager/expenses/migrations/0019_account_is_savings_budgetblock.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.10 on 2026-10-18 14:51 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('expenses', '0018_category_expense_type'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='account', + name='is_savings', + field=models.BooleanField(default=False, help_text='Márcala si es una cuenta donde guardas dinero: los traspasos que entran en ella cuentan como ahorro en el reparto por bloques.', verbose_name='Cuenta de ahorro'), + ), + migrations.CreateModel( + name='BudgetBlock', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('block', models.CharField(choices=[('fixed_essential', 'Fijo esencial'), ('fixed_discretionary', 'Fijo prescindible'), ('variable_essential', 'Variable esencial'), ('variable_discretionary', 'Variable prescindible'), ('saving', 'Ahorro')], max_length=30)), + ('percentage', models.DecimalField(decimal_places=2, default=0, max_digits=5, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(100)])), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='budget_blocks', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('owner', 'block')}, + }, + ), + ] diff --git a/expenses_manager/expenses/models.py b/expenses_manager/expenses/models.py index eb0ca44..0265787 100644 --- a/expenses_manager/expenses/models.py +++ b/expenses_manager/expenses/models.py @@ -1,6 +1,7 @@ from datetime import date from decimal import Decimal from django.core.exceptions import ObjectDoesNotExist, ValidationError +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models, transaction from django.conf import settings from django.db.models import Q, Sum @@ -114,6 +115,14 @@ class Account(models.Model): name = models.CharField(max_length=100) initial_balance = models.DecimalField(max_digits=12, decimal_places=2, default=0) active = models.BooleanField(default=True) + is_savings = models.BooleanField( + default=False, + verbose_name="Cuenta de ahorro", + help_text=( + "Márcala si es una cuenta donde guardas dinero: los traspasos que " + "entran en ella cuentan como ahorro en el reparto por bloques." + ), + ) created_at = models.DateTimeField(auto_now_add=True) class Meta: @@ -823,3 +832,35 @@ class Goal(models.Model): def __str__(self): return self.name + + +class BudgetBlock(models.Model): + """Qué porcentaje del ingreso debería ir a un bloque. + + Los bloques son los cuatro tipos de gasto de Category más el ahorro: lo que + el usuario clasificó en sus categorías es lo que ve aquí, sin un tercer eje. + + Un único reparto por usuario, sin versionar por fecha: un histórico obliga + a decidir qué reparto aplica a cada mes, y eso se añade el día que haga + falta. Un bloque sin fila vale 0. + """ + + BLOCK_SAVING = "saving" + BLOCK_CHOICES = Category.EXPENSE_TYPE_CHOICES + [(BLOCK_SAVING, "Ahorro")] + + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="budget_blocks" + ) + block = models.CharField(max_length=30, choices=BLOCK_CHOICES) + percentage = models.DecimalField( + max_digits=5, + decimal_places=2, + default=0, + validators=[MinValueValidator(0), MaxValueValidator(100)], + ) + + class Meta: + unique_together = ("owner", "block") + + def __str__(self): + return f"{self.get_block_display()}: {self.percentage} %"