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.
153 lines
6.3 KiB
Python
153 lines
6.3 KiB
Python
"""
|
||
tierra.py — El valor de las fincas rústicas, con el precio de la tierra del
|
||
Ministerio de Agricultura (MAPA).
|
||
|
||
El MAPA publica cada año el precio medio de la tierra en euros por hectárea,
|
||
por comunidad autónoma y por cultivo, con el secano y el regadío por separado
|
||
(serie_tierra.json, sacada de su informe en PDF: se actualiza con cada versión
|
||
del programa porque sale una vez al año). Un terreno rústico lleva en su ficha
|
||
(activo["inmueble"]), además de lo de cualquier inmueble:
|
||
|
||
superficie en hectáreas
|
||
cultivos [{desde, cultivo}]: el cultivo (código del MAPA) desde cada
|
||
fecha; «desde» vacío = desde siempre. Pasar de secano a
|
||
regadío es un cambio de cultivo con su fecha.
|
||
origen «compra»: el valor parte de lo que costó (o de la última
|
||
tasación apuntada a mano); «media»: parte de la superficie por
|
||
el precio medio de su cultivo y su comunidad.
|
||
|
||
Con «auto», el valor se mueve cada 31 de diciembre con lo que varió el precio
|
||
de su cultivo en su comunidad (si allí no hay dato, el nacional), y al cambiar
|
||
de cultivo se multiplica por lo que vale el nuevo frente al viejo ese año. Se
|
||
guardan como precios con la fuente «MAPA».
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import datetime as dt
|
||
import json
|
||
from functools import lru_cache
|
||
from pathlib import Path
|
||
|
||
SERIE = Path(__file__).resolve().parent / "serie_tierra.json"
|
||
FUENTE = "MAPA"
|
||
ORIGENES = {"compra": "Lo que costó o la última tasación", "media": "La media de su cultivo en su comunidad"}
|
||
|
||
|
||
@lru_cache(maxsize=1)
|
||
def serie() -> dict:
|
||
return json.loads(SERIE.read_text(encoding="utf-8"))
|
||
|
||
|
||
def cultivos() -> dict[str, str]:
|
||
return serie()["cultivos"]
|
||
|
||
|
||
def ultimo_año(s: dict | None = None) -> int:
|
||
s = s or serie()
|
||
return max(a for c in s["precios"]["nacional"].values() for a, _ in c)
|
||
|
||
|
||
def _f(v) -> float:
|
||
try:
|
||
return float(v or 0)
|
||
except (TypeError, ValueError):
|
||
return 0.0
|
||
|
||
|
||
def precio(comunidad: str, cultivo: str, año: int, s: dict | None = None) -> tuple[float, str] | None:
|
||
"""(€/ha, de dónde) para ese año: el de su comunidad o, si allí no hay dato
|
||
de ese cultivo, el nacional. Antes del primer año, el primero; después del
|
||
último publicado, el último."""
|
||
s = s or serie()
|
||
for donde in (comunidad, "nacional"):
|
||
datos = (s["precios"].get(donde) or {}).get(cultivo)
|
||
if datos:
|
||
valor = datos[0][1]
|
||
for a, v in datos:
|
||
if a > año:
|
||
break
|
||
valor = v
|
||
return float(valor), donde
|
||
return None
|
||
|
||
|
||
def cultivo_en(f: dict, dia: str) -> str | None:
|
||
"""El cultivo que tenía ese día según la ficha."""
|
||
actual = None
|
||
for c in sorted(f.get("cultivos") or [], key=lambda c: c.get("desde") or ""):
|
||
if (c.get("desde") or "") <= dia:
|
||
actual = c.get("cultivo")
|
||
elif actual is None:
|
||
actual = c.get("cultivo") # antes del primero conocido, el primero
|
||
return actual
|
||
|
||
|
||
def es_rustico(a: dict) -> bool:
|
||
return a.get("tipo") == "Inmueble" and (a.get("inmueble") or {}).get("clase") == "terreno_rustico"
|
||
|
||
|
||
def valor_medio(f: dict, dia: str, s: dict | None = None) -> float | None:
|
||
"""Superficie × precio medio de su cultivo y su comunidad ese año."""
|
||
ha, cultivo = _f(f.get("superficie")), cultivo_en(f, dia)
|
||
p = precio(f.get("comunidad") or "nacional", cultivo, int(dia[:4]), s) if ha and cultivo else None
|
||
return round(ha * p[0], 2) if p else None
|
||
|
||
|
||
def punto_de_partida(a: dict, movs: list[dict], s: dict | None = None) -> tuple[str, float] | None:
|
||
"""(fecha, valor) del día de la compra. Con origen «media», la superficie
|
||
por el precio medio; si no, lo que costó (como cualquier inmueble)."""
|
||
from .inmuebles import punto_de_partida as de_inmueble
|
||
partida = de_inmueble(a, movs)
|
||
f = a.get("inmueble") or {}
|
||
if partida and f.get("origen") == "media":
|
||
media = valor_medio(f, partida[0], s)
|
||
if media:
|
||
return partida[0], media
|
||
return partida
|
||
|
||
|
||
def estimaciones(a: dict, movs: list[dict], precios: list[dict], hoy: dt.date, s: dict | None = None) -> list[dict]:
|
||
"""El valor estimado de cada 31 de diciembre: [{fecha, valor_total}].
|
||
|
||
Se parte de la compra y de cada valor apuntado a mano; cada tramo se mueve
|
||
con el precio de su cultivo hasta el siguiente valor a mano, la venta o el
|
||
último año publicado. Un cambio de cultivo reescala el valor ese día."""
|
||
s = s or serie()
|
||
f = a.get("inmueble") or {}
|
||
if not es_rustico(a) or not f.get("auto"):
|
||
return []
|
||
partida = punto_de_partida(a, movs, s)
|
||
if partida is None or not cultivo_en(f, partida[0]):
|
||
return []
|
||
comunidad = f.get("comunidad") or "nacional"
|
||
inicio = partida[0]
|
||
eventos = [(r["fecha"], "valor", _f(r.get("valor_total") or r.get("precio"))) for r in precios
|
||
if r.get("fuente") == "manual" and _f(r.get("valor_total") or r.get("precio")) > 0 and r["fecha"] > inicio]
|
||
eventos += [(c["desde"], "cultivo", c["cultivo"]) for c in f.get("cultivos") or []
|
||
if (c.get("desde") or "") > inicio and c.get("cultivo")]
|
||
eventos.sort(key=lambda e: (e[0], e[1] == "valor")) # el mismo día, manda el valor a mano
|
||
venta = min((m["fecha"] for m in movs if m["tipo"] == "Venta"), default=None)
|
||
fin = min(hoy.isoformat(), venta) if venta else hoy.isoformat()
|
||
ultimo = ultimo_año(s)
|
||
|
||
def p(cultivo: str, año: int) -> float:
|
||
r = precio(comunidad, cultivo, año, s)
|
||
return r[0] if r else 0.0
|
||
|
||
salida: list[dict] = []
|
||
desde, valor, cultivo = inicio, partida[1], cultivo_en(f, inicio)
|
||
for fecha, clase, dato in eventos + [(fin, "fin", None)]:
|
||
base = p(cultivo, int(desde[:4]))
|
||
for año in range(int(desde[:4]), min(int(fecha[:4]), ultimo) + 1):
|
||
dia = f"{año}-12-31"
|
||
if desde <= dia < fecha and dia < fin and base:
|
||
salida.append({"fecha": dia, "valor_total": round(valor * p(cultivo, año) / base, 2)})
|
||
if clase == "fin":
|
||
break
|
||
if clase == "valor":
|
||
desde, valor = fecha, dato
|
||
elif dato != cultivo and base:
|
||
año = int(fecha[:4])
|
||
valor, desde, cultivo = valor * p(dato, año) / base, fecha, dato
|
||
return salida
|