""" regularizar.py — Ponerse al día: lo que costaría declarar ahora, por tu cuenta, lo que no se declaró en su momento, ejercicio a ejercicio. Para cada año cuyo plazo de declaración ya pasó y que aún no ha prescrito: 1. Lo que sale por la parte del ahorro (cuota.py), menos lo que ya pagaste por eso en tu declaración (lo dices tú; si no declaraste nada, cero). 2. El rendimiento de las actividades (actividad.py) por TU tipo marginal de ese año, que también dices tú: la parte general depende de lo demás que ganaste y la cartera no lo sabe. 3. Si la suma sale a pagar, el recargo por presentarla tarde SIN que te lo hayan reclamado (crece con los meses de retraso y sustituye a la sanción) y los intereses de demora. Cuándo empiezan a correr depende del territorio y del retraso: «intereses_tramos» = [[hasta tantos meses de retraso, desde qué mes tras el plazo cuentan (null: no hay)], …]. Sin esa clave, corren desde «intereses_desde_meses» y solo pasado ese plazo. Si en un año sale a devolver, no hay recargo: se pide la rectificación. Los porcentajes, el plazo de prescripción y los tipos de interés están en reglas/.json → «regularizacion», con su cita. Dos cosas son aproximadas y se dicen en pantalla: el día en que acabó el plazo de cada campaña y la cuota de la actividad (por el tipo marginal). Es una estimación para saber de qué orden es la cifra, no la liquidación. """ from __future__ import annotations import datetime as dt def _mas_años(d: dt.date, años: int) -> dt.date: try: return d.replace(year=d.year + años) except ValueError: # 29 de febrero return d.replace(year=d.year + años, day=28) def _meses(desde: dt.date, hasta: dt.date) -> int: """Meses completos transcurridos entre dos fechas.""" n = (hasta.year - desde.year) * 12 + hasta.month - desde.month return n - 1 if hasta.day < desde.day else n def fin_de_plazo(ejercicio: int, reglas: dict) -> dt.date: """El último día para declarar ese ejercicio: el del fichero si está, o el aproximado.""" exacto = reglas.get("fin_plazo", {}).get(str(ejercicio)) mes, dia = (int(x) for x in reglas["fin_plazo_aproximado"].split("-")) return dt.date.fromisoformat(exacto) if exacto else dt.date(ejercicio + 1, mes, dia) def recargo(importe: float, plazo: dt.date, hoy: dt.date, reglas: dict) -> tuple[float, float]: """(porcentaje, euros) del recargo por declarar hoy algo cuyo plazo acabó en «plazo».""" meses = _meses(plazo, hoy) for hasta, pct in reglas["recargos"]: if hasta is None or meses < hasta: return pct, round(importe * pct / 100, 2) return 0.0, 0.0 def carencia(plazo: dt.date, hoy: dt.date, reglas: dict) -> int | None: """Desde qué mes tras el fin del plazo cuentan los intereses al declarar hoy; None si no hay.""" tramos = reglas.get("intereses_tramos") if tramos is None: n = reglas["intereses_desde_meses"] tramos = [[n, None], [None, n]] meses = _meses(plazo, hoy) for hasta, desde in tramos: if hasta is None or meses < hasta: return desde return None def intereses(importe: float, plazo: dt.date, hoy: dt.date, reglas: dict) -> float: """Intereses de demora: corren desde que pasan los meses de carencia tras el fin del plazo.""" carencia_ = carencia(plazo, hoy, reglas) if carencia_ is None: return 0.0 año, mes = divmod(plazo.year * 12 + plazo.month - 1 + carencia_, 12) try: desde = dt.date(año, mes + 1, plazo.day) except ValueError: desde = dt.date(año, mes + 1, 28) total, tipos = 0.0, reglas["interes_demora"] ultimo = tipos[max(tipos)] d = desde while d < hoy: fin = min(dt.date(d.year + 1, 1, 1), hoy) dias_año = (dt.date(d.year + 1, 1, 1) - dt.date(d.year, 1, 1)).days total += importe * tipos.get(str(d.year), ultimo) / 100 * (fin - d).days / dias_año d = fin return round(total, 2) def limpiar(declarado: dict | None) -> dict: """Lo que el usuario ha dicho de cada año, saneado: {"2023": {"pagado": €, "marginal": %}}.""" salida = {} for año, d in (declarado or {}).items(): if not (str(año).isdigit() and isinstance(d, dict)): continue fila = {} for k, tope in (("pagado", 1e9), ("marginal", 60.0)): try: v = float(d.get(k)) except (TypeError, ValueError): continue if 0 <= v <= tope: fila[k] = round(v, 2) if fila: salida[str(año)] = fila return salida def plan(años: list[dict], declarado: dict, hoy: dt.date, reglas: dict) -> dict: """El plan de regularización. años: uno por ejercicio, en orden, con «ejercicio», «ahorro» (lo que sale a pagar por el ahorro, ya restadas las retenciones), «actividades» ([{nombre, rendimiento}]) y «verificado» (las reglas de ese año).""" declarado = limpiar(declarado) filas = [] for a in años: y = a["ejercicio"] plazo = fin_de_plazo(y, reglas) prescribe = _mas_años(plazo, reglas["prescripcion_años"]) suyo = declarado.get(str(y), {}) pagado, marginal = suyo.get("pagado", 0.0), suyo.get("marginal") rendimiento = round(sum(x["rendimiento"] for x in a["actividades"]), 2) fila = {"ejercicio": y, "fin_plazo": plazo.isoformat(), "prescribe": prescribe.isoformat(), "ahorro": a["ahorro"], "pagado": pagado, "rendimiento": rendimiento, "marginal": marginal, "verificado": a["verificado"]} if hoy <= plazo: fila["estado"] = "en_plazo" elif hoy > prescribe: fila["estado"] = "prescrito" else: general = round(rendimiento * marginal / 100, 2) if marginal is not None else None diferencia = round(a["ahorro"] - pagado + (general or 0.0), 2) fila.update({"estado": "abierto", "general": general, "diferencia": diferencia, "falta_marginal": bool(rendimiento) and marginal is None}) if diferencia > 0: pct, euros = recargo(diferencia, plazo, hoy, reglas) # Según el retraso, el recargo excluye los intereses o no (ver carencia) interes = intereses(diferencia, plazo, hoy, reglas) fila.update({"recargo_pct": pct, "recargo": euros, "intereses": interes, "total": round(diferencia + euros + interes, 2)}) else: fila.update({"recargo_pct": 0.0, "recargo": 0.0, "intereses": 0.0, "total": diferencia}) filas.append(fila) abiertas = [f for f in filas if f["estado"] == "abierto"] return { "hoy": hoy.isoformat(), "filas": filas, "total": {k: round(sum(f[k] for f in abiertas), 2) for k in ("diferencia", "recargo", "intereses", "total")}, "falta_marginal": any(f["falta_marginal"] for f in abiertas), "proxima_prescripcion": min((f["prescribe"] for f in abiertas), default=None), "citas": reglas["citas"], "texto_recargos": reglas["texto_recargos"], "nota": reglas["nota"], "norma": reglas["norma"], "fuente": reglas["fuente"], "consultado": reglas["consultado"], }