caudal/programa/cartera/fiscal/actividad.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

177 lines
9.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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"]))