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