caudal/programa/cartera/fiscal/__init__.py
esetheyers 7e18576f86 Caudal 0.2.0
Taxes for the whole of Spain (common territory, Navarre, Álava, Bizkaia and Gipuzkoa); flats, garages and land valued on their own; Treasury bills, bonds and deposits priced on BME; broker accounts; a guided start; a page per investment with its chart and the tax of selling today; net worth at the top; undo when deleting; Woodpecker CI for the tests and the Linux build.
2026-10-15 16:49:15 -05:00

124 lines
7.7 KiB
Python

"""
fiscal — Las cifras que hay que declarar, aparte de las de la cartera.
La cartera (calculo.py) contesta «¿cómo va mi inversión?» y usa el coste
medio. Hacienda pregunta otra cosa: «¿qué ganaste o perdiste en cada venta?»,
y para contestarla manda el FIFO: lo que se vende es lo que se compró primero.
Son dos cálculos con propósitos distintos y viven separados a propósito: este
paquete LEE las mismas operaciones y no cambia ni una cifra de la cartera.
fifo.py lotes de compra, ganancia o pérdida de cada transmisión,
rendimientos cobrados y existencias a 31 de diciembre.
cuota.py lo que saldría a pagar por la parte del ahorro.
actividad.py el rendimiento de una actividad económica (la minería), con
sus gastos y la amortización de los aparatos: parte general.
regularizar.py lo que costaría declarar ahora lo no declarado: recargos e intereses.
exportar.py el informe de un ejercicio en una hoja de cálculo.
reglas/ las reglas de cada territorio, por ejercicios, con su cita.
Es una ayuda para preparar la declaración, no asesoría fiscal. Cada criterio
que se aplica está escrito donde se aplica, con su fuente cuando la tiene.
"""
import datetime as dt
from . import alquiler, cuota, regularizar
from .fifo import CRITERIOS, OPCIONES, ejercicios, informe, opciones_validas
__all__ = ["CRITERIOS", "OPCIONES", "completo", "cuentas_bróker", "cuota", "ejercicios", "informe", "opciones_validas",
"regularizacion", "regularizar"]
def cuentas_bróker(activos: list[dict], movimientos: list[dict], ejercicio: int, trading: dict | None) -> list[dict]:
"""Las cuentas de bróker o trading con algo apuntado hasta ese año, con el
resultado que el usuario ha copiado del informe fiscal del bróker (o None).
«trading» es {"2025": {id del activo: euros}}."""
del_año = (trading or {}).get(str(ejercicio)) or {}
hasta = f"{ejercicio}-12-31"
con_algo = {m["activo"] for m in movimientos if m["fecha"] <= hasta}
return [{"activo": a["id"], "nombre": a["nombre"], "resultado": del_año.get(a["id"])}
for a in activos if a["tipo"] == "CuentaTrading" and a["id"] in con_algo]
def _resultado_bróker(activos, movimientos, ejercicio, trading) -> float:
return round(sum(c["resultado"] or 0.0 for c in cuentas_bróker(activos, movimientos, ejercicio, trading)), 2)
def completo(activos: list[dict], movimientos: list[dict], precios: list[dict], ejercicio: int,
opciones: dict | None = None, territorio: str | None = None, trading: dict | None = None) -> dict:
"""El informe del ejercicio con la liquidación de la parte del ahorro.
Para arrastrar bien las pérdidas pendientes hay que calcular todos los
años desde el primero con operaciones hasta ese. El territorio es el
elegido en las opciones, salvo que se pida otro."""
territorio = territorio or opciones_validas(opciones)["territorio"]
citas = cuota.reglas(territorio)["citas"]
fuera = set(cuota.reglas(territorio)["fuera_del_ahorro"])
# Los inmuebles: lo que su alquiler lleva cada año a la base del ahorro (en el País Vasco) y a la general
inm = alquiler.calcular(activos, movimientos, precios, ejercicio, cuota.reglas(territorio))
años, pedido = [], None
primero = min([int(m["fecha"][:4]) for m in movimientos] + [ejercicio])
for y in range(primero, ejercicio + 1):
r = informe(activos, movimientos, precios, y, opciones, citas,
cuota.reglas(territorio)["actividad"] if y == ejercicio else None,
cuota.reglas(territorio).get("recompra_cripto"), cuota.reglas(territorio).get("coeficientes"))
al_ahorro = [c for c in r["por_concepto"] if c["concepto"] not in fuera]
años.append({
"ejercicio": y,
"rendimientos": sum(c["bruto"] for c in al_ahorro) + inm["ahorro"].get(y, 0.0),
"retenciones": sum(c["retencion"] for c in al_ahorro),
# Lo que da el bróker de cada cuenta de trading, y la venta de un inmueble, también son ganancias y pérdidas
"ganancias": r["total"]["computable"] + _resultado_bróker(activos, movimientos, y, trading)
+ sum(v["ganancia"] for v in inm["ventas"].get(y, [])),
"incrementos": sum(t["ganancia"] for t in r["transmisiones"] if t["ganancia"] > 0),
"transmitido": sum(t["valor_transmision"] for t in r["transmisiones"]),
# Sin saber cuánto se vendió dentro de la cuenta, la exención por vender poco no se aplica
"sin_exencion": bool(_resultado_bróker(activos, movimientos, y, trading)),
})
pedido = r
pedido["cuota"] = cuota.ahorro(territorio, años)
pedido["cuota"]["fuera_del_ahorro"] = [c for c in pedido["por_concepto"] if c["concepto"] in fuera]
pedido["bróker"] = cuentas_bróker(activos, movimientos, ejercicio, trading)
con_dato = {c["activo"] for c in pedido["bróker"] if c["resultado"] is not None}
pedido["fuera"] = [f for f in pedido["fuera"] if f["activo"] not in con_dato]
pedido["alquileres"], pedido["imputaciones"] = inm["fichas"], inm["imputaciones"]
pedido["ventas_inmuebles"] = inm["ventas"].get(ejercicio, [])
pedido["cuota"]["alquiler_ahorro"] = inm["ahorro"].get(ejercicio, 0.0)
return pedido
def regularizacion(activos: list[dict], movimientos: list[dict], precios: list[dict], hoy: dt.date,
opciones: dict | None = None, declarado: dict | None = None, territorio: str | None = None,
trading: dict | None = None) -> dict:
"""Lo que costaría ponerse al día de cada ejercicio ya vencido y no prescrito
(ver regularizar.py). «declarado» es lo que el usuario dice de cada año."""
territorio = territorio or opciones_validas(opciones)["territorio"]
reglas = cuota.reglas(territorio)
fuera = set(reglas["fuera_del_ahorro"])
años, filas = [], []
inm = alquiler.calcular(activos, movimientos, precios, hoy.year, reglas) if movimientos else {"ahorro": {}, "general": {}, "ventas": {}}
if movimientos:
for y in range(min(int(m["fecha"][:4]) for m in movimientos), hoy.year + 1):
r = informe(activos, movimientos, precios, y, opciones, reglas["citas"], reglas["actividad"],
reglas.get("recompra_cripto"), reglas.get("coeficientes"))
al_ahorro = [c for c in r["por_concepto"] if c["concepto"] not in fuera]
años.append({
"ejercicio": y,
"rendimientos": sum(c["bruto"] for c in al_ahorro) + inm["ahorro"].get(y, 0.0),
"retenciones": sum(c["retencion"] for c in al_ahorro),
"ganancias": r["total"]["computable"] + _resultado_bróker(activos, movimientos, y, trading)
+ sum(v["ganancia"] for v in inm["ventas"].get(y, [])),
"incrementos": sum(t["ganancia"] for t in r["transmisiones"] if t["ganancia"] > 0),
"transmitido": sum(t["valor_transmision"] for t in r["transmisiones"]),
"sin_exencion": bool(_resultado_bróker(activos, movimientos, y, trading)),
})
liquidacion = cuota.ahorro(territorio, años)
filas.append({"ejercicio": y, "ahorro": liquidacion["resultado"], "verificado": liquidacion["verificado"],
# A tu tipo marginal: la minería y lo que el alquiler y la renta imputada llevan a la parte general
"actividades": [{"nombre": a["nombre"], "rendimiento": a["rendimiento"]} for a in r["actividades"]]
+ inm["general"].get(y, [])})
salida = regularizar.plan(filas, declarado or {}, hoy, reglas["regularizacion"])
salida["nombre_territorio"] = cuota.TERRITORIOS[territorio]
salida["sin_desembolso"] = opciones_validas(opciones)["sin_desembolso"]
return salida