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.
177 lines
9.1 KiB
Python
177 lines
9.1 KiB
Python
"""
|
||
actividad.py — El rendimiento de una actividad económica (la minería), año a
|
||
año: lo que va a la parte GENERAL de la declaración, no a la del ahorro.
|
||
|
||
Minar es una actividad económica (DGT V3625-16, V1748-18): lo minado es su
|
||
ingreso, por lo que valía el día que se recibió, y de él se restan los gastos
|
||
de la actividad. Se cuenta con las operaciones que llevan la etiqueta de la
|
||
actividad (la misma que usa actividades.py para la rentabilidad):
|
||
|
||
ingresos lo minado o recibido en el año, a su valor de ese día
|
||
gastos los gastos corrientes del año (hosting, luz, reparaciones),
|
||
menos lo que te hayan devuelto de ellos
|
||
amortización los aparatos no se restan de golpe: cada año, un porcentaje
|
||
de lo que costaron (el primero, en proporción a los días),
|
||
hasta agotarlo. Un gasto es un aparato si lleva la marca
|
||
«amortizable»; lo devuelto de un aparato baja su precio.
|
||
reducción en estimación directa simplificada, un porcentaje del
|
||
rendimiento cuando es positivo (con tope, donde lo haya)
|
||
rendimiento ingresos − gastos − amortización − reducción
|
||
|
||
En el País Vasco la modalidad simplificada no amortiza aparte («amortiza»:
|
||
false): la reducción cubre amortizaciones y gastos de difícil justificación,
|
||
y desde 2025 o 2026, según el territorio, va por tramos («reduccion_tramos»):
|
||
o por partes de la diferencia, como un IRPF en pequeño («por_partes»), o con
|
||
un solo porcentaje según la diferencia del año anterior («año_anterior»).
|
||
Las reglas que cambian con los años van en «ejercicios», como en cuota.py.
|
||
|
||
Si sale negativo, resta del resto de la parte general de ese año.
|
||
|
||
Aquí no se calcula la cuota: la parte general depende de lo demás que ganes
|
||
(tu trabajo), que la cartera no conoce. Los porcentajes están en
|
||
reglas/<territorio>.json → «actividad», con su cita.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import datetime as dt
|
||
|
||
from ..actividades import clave
|
||
|
||
|
||
def _dias_del_año(año: int) -> int:
|
||
return (dt.date(año + 1, 1, 1) - dt.date(año, 1, 1)).days
|
||
|
||
|
||
def _aparatos(gastos: list[dict]) -> list[dict]:
|
||
"""Los aparatos de una actividad con su precio final: lo devuelto de un
|
||
aparato se descuenta del más antiguo que aún tenga precio."""
|
||
aparatos = [{"movimiento": g["movimiento"], "fecha": g["fecha"], "notas": g["notas"], "coste": g["importe"],
|
||
"estimado": g["estimado"]} for g in gastos if g["importe"] > 0]
|
||
for g in gastos:
|
||
falta = -g["importe"]
|
||
for a in aparatos:
|
||
if falta <= 0.004:
|
||
break
|
||
q = min(a["coste"], falta)
|
||
a["coste"] = round(a["coste"] - q, 2)
|
||
a["estimado"] = a["estimado"] or g["estimado"]
|
||
falta -= q
|
||
return [a for a in aparatos if a["coste"] > 0.004]
|
||
|
||
|
||
def amortizar(aparato: dict, ejercicio: int, coeficiente: float) -> dict:
|
||
"""Lo amortizado antes de ese ejercicio y en él, por tablas y en línea
|
||
recta: coste × coeficiente cada año, el primero en proporción a los días
|
||
desde que se compró, hasta agotar el coste."""
|
||
compra = dt.date.fromisoformat(aparato["fecha"])
|
||
antes = este = 0.0
|
||
for año in range(compra.year, ejercicio + 1):
|
||
cuota = aparato["coste"] * coeficiente / 100
|
||
if año == compra.year:
|
||
cuota *= ((dt.date(año, 12, 31) - compra).days + 1) / _dias_del_año(año)
|
||
cuota = round(min(cuota, aparato["coste"] - antes), 2)
|
||
if año < ejercicio:
|
||
antes = round(antes + cuota, 2)
|
||
else:
|
||
este = cuota
|
||
return {**aparato, "antes": antes, "amortizacion": este, "pendiente": round(aparato["coste"] - antes - este, 2)}
|
||
|
||
|
||
def del_ejercicio(reglas: dict, ejercicio: int) -> dict:
|
||
"""Las reglas de la actividad que rigen ese año: las generales con lo que
|
||
cambie el primer tramo de «ejercicios» cuyo «desde» no lo supere."""
|
||
base = {k: v for k, v in reglas.items() if k != "ejercicios"}
|
||
for tramo in reglas.get("ejercicios", []):
|
||
if ejercicio >= tramo["desde"]:
|
||
return {**base, **tramo, "citas": {**base.get("citas", {}), **tramo.get("citas", {})}}
|
||
return base
|
||
|
||
|
||
def _eur(v: float) -> str:
|
||
return f"{v:,.0f}".replace(",", ".") + " €"
|
||
|
||
|
||
def reduccion(previo: float, anterior: float | None, reglas: dict) -> tuple[float, float, str]:
|
||
"""(reducción, porcentaje efectivo, cómo se ha calculado) de la estimación
|
||
directa simplificada sobre un rendimiento previo positivo. «anterior» es
|
||
la diferencia entre ingresos y gastos del año anterior, si lo hubo."""
|
||
if previo <= 0:
|
||
return 0.0, 0.0, ""
|
||
tramos = reglas.get("reduccion_tramos")
|
||
if not tramos:
|
||
pct = reglas["reduccion_simplificada"]
|
||
tope = reglas.get("reduccion_maxima")
|
||
importe = round(min(previo * pct, tope or previo), 2)
|
||
texto = f"Reducción del {pct:.0%}" + (f", con un máximo de {_eur(tope)}" if tope else "")
|
||
return importe, pct, texto
|
||
if reglas.get("reduccion_modo") == "año_anterior":
|
||
mira = anterior if anterior is not None else previo
|
||
pct = next(p for hasta, p in tramos if hasta is None or mira <= hasta)
|
||
cual = "del año anterior" if anterior is not None else "de este año (no hay año anterior)"
|
||
return round(previo * pct, 2), pct, f"Reducción del {pct:.0%}, por la diferencia entre ingresos y gastos {cual}"
|
||
importe, desde = 0.0, 0.0
|
||
for hasta, p in tramos:
|
||
if previo <= desde:
|
||
break
|
||
importe += ((previo if hasta is None else min(previo, hasta)) - desde) * p
|
||
if hasta is None:
|
||
break
|
||
desde = hasta
|
||
importe = round(importe, 2)
|
||
partes = ", ".join(f"{p:.0%} hasta {_eur(h)}" if h else f"{p:.0%} del resto" for h, p in tramos)
|
||
return importe, round(importe / previo, 4), f"Reducción por partes ({partes})"
|
||
|
||
|
||
def rendimientos(gastos: list[dict], recibido: list[dict], ejercicio: int, opciones: dict, reglas: dict) -> list[dict]:
|
||
"""Una ficha por actividad con algo que contar ese ejercicio.
|
||
|
||
gastos y recibido son los de TODOS los años (los que apunta el motor del
|
||
FIFO): un aparato de hace tres años se sigue amortizando este."""
|
||
reglas = del_ejercicio(reglas, ejercicio)
|
||
amortiza = reglas.get("amortiza", True)
|
||
tablas = reglas.get("amortizacion") or {}
|
||
tabla = (tablas.get(str(opciones.get("amortizacion"))) or next(iter(tablas.values()))) if amortiza else None
|
||
año = str(ejercicio)
|
||
nombres: dict[str, str] = {}
|
||
for x in gastos + recibido:
|
||
if x.get("actividad"):
|
||
nombres.setdefault(clave(x["actividad"]), x["actividad"])
|
||
fichas = []
|
||
for k, nombre in nombres.items():
|
||
suyos = [g for g in gastos if clave(g["actividad"]) == k]
|
||
entradas = [r for r in recibido if clave(r["actividad"]) == k and r["fecha"].startswith(año)]
|
||
corrientes = [g for g in suyos if not g["amortizable"] and g["fecha"].startswith(año)]
|
||
comprados = _aparatos([g for g in suyos if g["amortizable"] and g["fecha"] <= f"{año}-12-31"])
|
||
if amortiza:
|
||
aparatos = [amortizar(a, ejercicio, tabla["coeficiente"]) for a in comprados]
|
||
aparatos = [a for a in aparatos if a["amortizacion"] or a["pendiente"] or a["fecha"].startswith(año)]
|
||
else:
|
||
aparatos = []
|
||
if not (entradas or corrientes or any(a["amortizacion"] for a in aparatos)):
|
||
continue
|
||
ingresos = round(sum(r["valor"] for r in entradas), 2)
|
||
gasto = round(sum(g["importe"] for g in corrientes), 2)
|
||
amortizacion = round(sum(a["amortizacion"] for a in aparatos), 2)
|
||
previo = round(ingresos - gasto - amortizacion, 2)
|
||
# La diferencia del año anterior, para los territorios que eligen el porcentaje por ella
|
||
antes = str(ejercicio - 1)
|
||
entradas_antes = [r for r in recibido if clave(r["actividad"]) == k and r["fecha"].startswith(antes)]
|
||
gastos_antes = [g for g in suyos if not g["amortizable"] and g["fecha"].startswith(antes)]
|
||
anterior = round(sum(r["valor"] for r in entradas_antes) - sum(g["importe"] for g in gastos_antes), 2) \
|
||
if entradas_antes or gastos_antes else None
|
||
rebaja, porcentaje, texto_reduccion = reduccion(previo, anterior, reglas)
|
||
fichas.append({
|
||
"nombre": nombre, "clave": k, "ejercicio": ejercicio,
|
||
"ingresos": ingresos, "entradas": len(entradas), "cantidad": sum(r["cantidad"] for r in entradas),
|
||
"gastos": gasto, "corrientes": sorted(corrientes, key=lambda g: g["fecha"]),
|
||
"amortizacion": amortizacion, "aparatos": aparatos,
|
||
"tabla": tabla, "previo": previo, "reduccion": rebaja,
|
||
"porcentaje_reduccion": porcentaje, "texto_reduccion": texto_reduccion,
|
||
"amortiza": amortiza, "aparatos_sin_amortizar": 0 if amortiza else len(comprados),
|
||
"rendimiento": round(previo - rebaja, 2),
|
||
"estimados": sum(1 for x in corrientes + aparatos if x["estimado"]),
|
||
"verificado": reglas["verificado"], "nota": reglas["nota"], "citas": reglas["citas"],
|
||
"texto_negativo": reglas.get("texto_negativo"),
|
||
})
|
||
return sorted(fichas, key=lambda f: -abs(f["rendimiento"]))
|