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

153 lines
6.3 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.

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