Просмотр исходного кода

Administración de transacciones recurrentes

Celestino Rey 1 год назад
Родитель
Сommit
e92ccee014
6 измененных файлов с 91 добавлено и 4 удалено
  1. 1 1
      K8S/Makefile
  2. 1 1
      K8S/env-prod-configmap.yaml
  3. 3 1
      src/finanzas/admin.py
  4. 28 0
      src/finanzas/models.py
  5. 2 0
      src/finanzas/urls.py
  6. 56 1
      src/finanzas/views.py

+ 1 - 1
K8S/Makefile

@@ -1,6 +1,6 @@
 export ARQUITECTURA := $(shell lscpu |grep itectur | tr -d ' '| cut -f2 -d':')
 export REGISTRY=registry.reymota.es
-export IMG_VERSION = 0.26
+export IMG_VERSION = 0.27
 export IMG_NGINX_VERSION = 1.0
 
 # limpia todo

+ 1 - 1
K8S/env-prod-configmap.yaml

@@ -2,7 +2,7 @@ apiVersion: v1
 data:
   DEBUG: "False"
   ENTORNO: "Producción"
-  APP_VERSION: 15.0.12
+  APP_VERSION: 15.0.13
   SECRET_KEY: change_me
   SQL_HOST: postgresql
   SQL_PORT: "5432"

+ 3 - 1
src/finanzas/admin.py

@@ -1,9 +1,11 @@
 # budget/admin.py
 
 from django.contrib import admin
-from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria
+from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria, TransaccionRecurrente
+
 
 admin.site.register(Movimiento)
 admin.site.register(Cuenta)
 admin.site.register(MovimientoFrecuente)
 admin.site.register(Categoria)
+admin.site.register(TransaccionRecurrente)

+ 28 - 0
src/finanzas/models.py

@@ -48,3 +48,31 @@ class MovimientoFrecuente(models.Model):
 
     def __str__(self):
         return f"{self.nombre}: {self.cuenta_origen} → {self.cuenta_destino} ({self.monto} €)"
+
+
+class TransaccionRecurrente(models.Model):
+    FRECUENCIA_CHOICES = [
+        ('mensual_dia', 'Día fijo del mes'),
+        ('mensual_semana', 'Primer día específico de la semana del mes'),
+    ]
+
+    usuario = models.ForeignKey(ReyMotaUser, on_delete=models.CASCADE)
+    cuenta_origen = models.ForeignKey(Cuenta, on_delete=models.CASCADE, related_name='recurrente_salida')
+    cuenta_destino = models.ForeignKey(Cuenta, on_delete=models.CASCADE, related_name='recurrente_entrada')
+    monto = models.DecimalField(max_digits=10, decimal_places=2)
+    categoria = models.ForeignKey(Categoria, on_delete=models.SET_NULL, null=True, blank=True)
+    descripcion = models.TextField(blank=True, null=True)
+    frecuencia = models.CharField(max_length=20, choices=FRECUENCIA_CHOICES)
+
+    # Para frecuencia tipo mensual_dia
+    dia_del_mes = models.PositiveIntegerField(null=True, blank=True)  # 1-31
+
+    # Para frecuencia tipo mensual_semana
+    dia_semana = models.PositiveIntegerField(null=True, blank=True)  # 0=lunes ... 6=domingo
+    semana = models.PositiveIntegerField(null=True, blank=True)      # 1=primera, 2=segunda, etc.
+
+    activa = models.BooleanField(default=True)
+    ultima_ejecucion = models.DateField(null=True, blank=True)
+
+    def __str__(self):
+        return f"{self.descripcion or 'Recurrente'}: {self.monto} € cada mes"

+ 2 - 0
src/finanzas/urls.py

@@ -16,4 +16,6 @@ urlpatterns = [
     # API
     path('api/datos/', views.api_datos_completos, name='api_datos_completos'),
 
+    path('ejecutar-recurrentes/', views.ejecutar_recurrentes, name='ejecutar_recurrentes'),
+
 ]

+ 56 - 1
src/finanzas/views.py

@@ -6,12 +6,14 @@ from django.contrib.auth.decorators import login_required
 from django.contrib import messages
 from django.utils.timezone import now
 from django.core.serializers import serialize
+from datetime import date, timedelta
 
 import logging
 import re
+import calendar
 
 from accounts import models
-from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria
+from .models import Movimiento, Cuenta, MovimientoFrecuente, Categoria, TransaccionRecurrente
 from .forms import MovimientoForm, FiltroForm, CuentaForm
 
 logger = logging.getLogger(__name__)
@@ -273,3 +275,56 @@ def api_datos_completos(request):
         'favoritos': list(MovimientoFrecuente.objects.filter(usuario=request.user).values()),
     }
     return JsonResponse(data, safe=False, json_dumps_params={'ensure_ascii': False, 'indent': 2})
+
+
+def procesar_transacciones_recurrentes(hoy=None):
+    if hoy is None:
+        hoy = date.today()
+
+    for tr in TransaccionRecurrente.objects.filter(activa=True):
+        if tr.ultima_ejecucion and tr.ultima_ejecucion >= hoy.replace(day=1):
+            continue  # Ya se ejecutó este mes
+
+        ejecutar = False
+
+        if tr.frecuencia == 'mensual_dia' and tr.dia_del_mes:
+            ejecutar = hoy.day == tr.dia_del_mes
+
+        elif tr.frecuencia == 'mensual_semana' and tr.semana and tr.dia_semana is not None:
+            # Calcular fecha del primer (por ejemplo) lunes
+            primer_dia = hoy.replace(day=1)
+            weekday_count = 0
+            for i in range(31):  # máx 31 días
+                d = primer_dia + timedelta(days=i)
+                if d.month != hoy.month:
+                    break
+                if d.weekday() == tr.dia_semana:
+                    weekday_count += 1
+                    if weekday_count == tr.semana and d == hoy:
+                        ejecutar = True
+                        break
+
+        if ejecutar:
+            Movimiento.objects.create(
+                usuario=tr.usuario,
+                cuenta_origen=tr.cuenta_origen,
+                cuenta_destino=tr.cuenta_destino,
+                monto=tr.monto,
+                fecha=hoy,
+                descripcion=tr.descripcion,
+                categoria=tr.categoria,
+            )
+            tr.ultima_ejecucion = hoy
+            tr.save()
+
+            # Actualizar saldos
+            tr.cuenta_origen.saldo -= tr.monto
+            tr.cuenta_origen.save()
+            tr.cuenta_destino.saldo += tr.monto
+            tr.cuenta_destino.save()
+
+
+@login_required
+def ejecutar_recurrentes(request):
+    procesar_transacciones_recurrentes()
+    return redirect('dashboard')