caudal/programa/pruebas/test_inmuebles.py
2026-10-23 16:07:24 -05:00

327 lines
18 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.

"""
Pruebas de los inmuebles (cartera/inmuebles.py): el valor estimado con el
índice del INE, la vivienda protegida, la vivienda propia fuera de las
inversiones, las cifras del alquiler y la hipoteca vinculada.
python3 -m unittest pruebas.test_inmuebles -v
"""
from __future__ import annotations
import datetime as dt
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from cartera import inmuebles # noqa: E402
from cartera.datos import Datos, ErrorDatos, escribir, leer # noqa: E402
from cartera.operaciones import Cartera, ErrorOperacion # noqa: E402
from cartera.vista import resumen # noqa: E402
HOY = dt.date(2026, 10, 15)
SERIE = inmuebles.serie()
NAVARRA = SERIE["series"]["navarra"]["segunda_mano"]
def piso(**ficha):
return {"id": "IN1", "nombre": "Piso de la calle Mayor", "tipo": "Inmueble", "valoracion": "total",
"inmueble": {"clase": "vivienda", "uso": "alquilado", "comunidad": "navarra", "auto": True, **ficha}}
def compra(fecha="2020-05-10", importe=220000.0):
return {"id": "m1", "activo": "IN1", "tipo": "Compra", "fecha": fecha, "importe": importe}
class TestValorEstimado(unittest.TestCase):
def test_sigue_el_indice_de_su_comunidad_desde_la_compra(self):
e = inmuebles.estimaciones(piso(precio_compra=200000), [compra()], [], SERIE, HOY)
self.assertEqual(e[0]["fecha"], "2020-09-30") # el primer trimestre después del de la compra
base = inmuebles.indice(NAVARRA, "2020T2")
self.assertEqual(e[0]["valor_total"], round(200000 * inmuebles.indice(NAVARRA, "2020T3") / base, 2))
self.assertEqual(e[-1]["fecha"], inmuebles.fin_de_trimestre(NAVARRA[-1][0]).isoformat())
self.assertEqual(e[-1]["valor_total"], round(200000 * NAVARRA[-1][1] / base, 2))
def test_sin_precio_de_escritura_parte_de_la_compra_entera(self):
e = inmuebles.estimaciones(piso(), [compra()], [], SERIE, HOY)
self.assertAlmostEqual(e[0]["valor_total"] / 220000, inmuebles.indice(NAVARRA, "2020T3") / inmuebles.indice(NAVARRA, "2020T2"), places=6)
def test_una_tasacion_apuntada_a_mano_es_el_nuevo_punto_de_partida(self):
tasacion = {"activo": "IN1", "fecha": "2024-02-15", "valor_total": 300000.0, "fuente": "manual"}
e = inmuebles.estimaciones(piso(precio_compra=200000), [compra()], [tasacion], SERIE, HOY)
antes = [x for x in e if x["fecha"] < "2024-02-15"]
despues = [x for x in e if x["fecha"] > "2024-02-15"]
self.assertEqual(antes[-1]["fecha"], "2023-12-31")
self.assertEqual(despues[0]["valor_total"],
round(300000 * inmuebles.indice(NAVARRA, "2024T2") / inmuebles.indice(NAVARRA, "2024T1"), 2))
def test_una_vivienda_protegida_no_pasa_de_su_precio_maximo(self):
e = inmuebles.estimaciones(piso(precio_compra=200000, protegida=True, precio_maximo=205000), [compra()], [], SERIE, HOY)
self.assertEqual(max(x["valor_total"] for x in e), 205000.0)
def test_sin_valor_automatico_o_tras_venderlo_no_hay_estimaciones(self):
self.assertEqual(inmuebles.estimaciones(piso(auto=False), [compra()], [], SERIE, HOY), [])
venta = {"id": "m2", "activo": "IN1", "tipo": "Venta", "fecha": "2022-01-20", "importe": 250000.0}
e = inmuebles.estimaciones(piso(), [compra(), venta], [], SERIE, HOY)
self.assertEqual(e[-1]["fecha"], "2021-12-31")
def test_trimestres(self):
self.assertEqual((inmuebles.trimestre(dt.date(2025, 11, 3)), inmuebles.fin_de_trimestre("2025T1")),
("2025T4", dt.date(2025, 3, 31)))
self.assertEqual(inmuebles.indice(NAVARRA, "1999T1"), NAVARRA[0][1]) # antes de la serie, el primero
class ConCartera(unittest.TestCase):
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.addCleanup(self._tmp.cleanup)
carpeta = Path(self._tmp.name) / "datos"
escribir(carpeta, Datos([], [], [], []))
self.c = Cartera(carpeta)
self.addCleanup(self.c.cerrar)
def alta(self, **ficha):
f = {"clase": "vivienda", "uso": "alquilado", "comunidad": "navarra", "auto": True, **ficha}
return self.c.guardar_activo({"tipo": "Inmueble", "nombre": ficha.pop("nombre", "Piso"), "inmueble": f})
def mov(self, aid, tipo, fecha, importe, **extra):
return self.c.guardar_movimiento({"activo": aid, "tipo": tipo, "fecha": fecha, "importe": importe, **extra})
def ipv(self, aid):
return [r for r in self.c.datos.vivos("precios") if r["activo"] == aid and r.get("fuente") == "IPV"]
class TestEnLaCartera(ConCartera):
def test_al_apuntar_la_compra_se_guardan_los_valores_estimados(self):
a = self.alta(precio_compra=150000)
self.assertEqual((a["valoracion"], a["id"][:2]), ("total", "IN"))
self.mov(a["id"], "Compra", "2019-03-01", 165000)
self.assertGreater(len(self.ipv(a["id"])), 20)
# Al quitar la estimación, se borran
self.c.guardar_activo({"id": a["id"], "tipo": "Inmueble", "nombre": "Piso",
"inmueble": {"clase": "vivienda", "uso": "alquilado", "auto": False}})
self.assertEqual(self.ipv(a["id"]), [])
def test_un_valor_a_mano_no_se_pisa(self):
a = self.alta()
self.mov(a["id"], "Compra", "2019-03-01", 165000)
self.c.precio_manual(a["id"], "2025-06-30", 400000)
r = [x for x in self.c.datos.vivos("precios") if x["fecha"] == "2025-06-30"]
self.assertEqual([(x["fuente"], x["valor_total"]) for x in r], [("manual", 400000.0)])
def test_protegida_sin_precio_maximo_no_se_acepta(self):
with self.assertRaises(ErrorOperacion):
self.alta(protegida=True)
with self.assertRaises(ErrorOperacion):
self.alta(hipoteca="D99")
def test_los_datos_rechazan_una_ficha_rota(self):
a = self.alta()
a["inmueble"]["clase"] = "castillo"
with self.assertRaises(ErrorOperacion):
self.c._guardar()
self.assertEqual(leer(self.c.carpeta).vivos("activos")[0]["inmueble"]["clase"], "vivienda")
class TestEnElResumen(ConCartera):
def test_alquiler_gastos_y_rentabilidad_de_los_ultimos_doce_meses(self):
a = self.alta(auto=False)
self.mov(a["id"], "Compra", "2020-01-10", 200000)
for mes in range(1, 13):
self.mov(a["id"], "Dividendo", f"2026-{mes:02d}-05" if mes <= 9 else f"2025-{mes:02d}-20", 800)
self.mov(a["id"], "Gasto", "2026-04-01", 600, notas="IBI")
self.mov(a["id"], "Gasto", "2026-05-01", 10000, notas="Cocina", amortizable=True)
r = resumen(self.c.datos, HOY)
f = next(x for x in r["abiertos"] if x["id"] == a["id"])
self.assertEqual((f["alquiler_12m"], f["gastos_12m"], f["compra"]), (9600.0, 600.0, 210000.0))
self.assertEqual(f["rentabilidad_neta"], round(9000 / 210000, 4))
self.assertEqual(r["inmuebles"]["numero"], 1)
def test_tu_casa_cuenta_en_el_patrimonio_y_no_como_inversion(self):
casa = self.alta(uso="propio", auto=False, nombre="Mi casa")
self.mov(casa["id"], "Compra", "2015-06-01", 180000)
self.c.precio_manual(casa["id"], "2026-01-10", 260000)
r = resumen(self.c.datos, HOY)
self.assertEqual([x["id"] for x in r["abiertos"]], [])
self.assertEqual((r["bienes"][0]["valor"], r["bienes"][0]["revalorizacion"]), (260000.0, 80000.0))
self.assertEqual((r["global"]["valor"], r["global"]["patrimonio"]), (0, 260000.0))
self.assertFalse(any(x["valor"] for x in r["evolucion"])) # la gráfica es de las inversiones
self.assertFalse(any(x["aportado"] for x in r["evolucion"])) # y lo aportado, también sin la casa
def test_la_grafica_cuenta_lo_aportado_de_las_mismas_inversiones_que_el_valor(self):
casa = self.alta(uso="propio", auto=False, nombre="Mi casa")
self.mov(casa["id"], "Compra", "2015-06-01", 180000)
piso = self.alta(auto=False, nombre="Piso alquilado")
self.mov(piso["id"], "Compra", "2020-01-10", 100000)
self.c.precio_manual(piso["id"], "2026-01-10", 120000)
ultimo = resumen(self.c.datos, HOY)["evolucion"][-1]
self.assertEqual((ultimo["valor"], ultimo["aportado"]), (120000.0, 100000.0))
def test_la_hipoteca_vinculada_resta_y_sus_intereses_son_gasto(self):
d = self.c.guardar_activo({"tipo": "Deuda", "nombre": "Hipoteca del piso", "fecha_inicio": "2025-01-15",
"capital": 100000, "tin": 3, "cuotas": 240, "primera_cuota": "2025-02-01"})
a = self.alta(auto=False, hipoteca=d["id"])
self.mov(a["id"], "Compra", "2020-01-10", 200000)
self.mov(a["id"], "Dividendo", "2026-03-05", 9000)
r = resumen(self.c.datos, HOY)
f = next(x for x in r["abiertos"] if x["id"] == a["id"])
pendiente = r["deudas"][0]["pendiente"]
self.assertEqual((f["hipoteca_pendiente"], f["neto"]), (pendiente, round(200000 - pendiente, 2)))
self.assertGreater(f["intereses_12m"], 2500) # unos 3.000 € al 3 % sobre 100.000
self.assertEqual(f["gastos_12m"], f["intereses_12m"])
class TestAlquilerAutomatico(ConCartera):
def test_la_renta_mensual_se_cobra_sola_y_se_para_al_dejar_de_alquilar(self):
a = self.alta(auto=False, renta_mensual=750, dia_cobro=5, renta_desde="2026-07-01")
self.mov(a["id"], "Compra", "2020-01-10", 200000)
self.c.generar_recurrentes(HOY)
cobros = [m for m in self.c.datos.vivos("movimientos") if m["activo"] == a["id"] and m["tipo"] == "Dividendo"]
self.assertEqual([(m["fecha"], m["importe"]) for m in cobros],
[("2026-07-05", 750), ("2026-08-05", 750), ("2026-09-05", 750), ("2026-10-05", 750)])
self.c.guardar_activo({"id": a["id"], "tipo": "Inmueble", "nombre": "Piso",
"inmueble": {"clase": "vivienda", "uso": "vacio"}})
r = next(x for x in self.c.datos.vivos("recurrentes") if x["activo"] == a["id"])
self.assertEqual(r["hasta"], dt.date.today().isoformat())
self.assertEqual(len([m for m in self.c.datos.vivos("movimientos") if m["tipo"] == "Dividendo"]), 4)
def test_el_dia_de_cobro_va_del_1_al_28(self):
with self.assertRaises(ErrorOperacion):
self.alta(renta_mensual=700, dia_cobro=31)
class TestImpagos(ConCartera):
def test_un_impago_no_cuenta_como_cobrado_y_se_avisa(self):
a = self.alta(auto=False)
self.mov(a["id"], "Compra", "2020-01-10", 200000)
self.mov(a["id"], "Dividendo", "2026-08-05", 800)
self.mov(a["id"], "Dividendo", "2026-09-05", 800, impagado=True)
r = resumen(self.c.datos, HOY)
f = next(x for x in r["abiertos"] if x["id"] == a["id"])
self.assertEqual((f["alquiler_12m"], f["dividendos"]), (800.0, 800.0))
self.assertTrue(any(x["tipo"] == "impagado" and "800,00" in x["texto"] for x in r["avisos"]))
def test_hacienda_lo_cuenta_y_pasados_seis_meses_se_resta_en_territorio_comun(self):
from cartera import fiscal
a = self.alta(auto=False, porcentaje_construccion=60)
self.mov(a["id"], "Compra", "2020-01-10", 200000)
self.mov(a["id"], "Dividendo", "2025-03-05", 800, impagado=True)
self.mov(a["id"], "Dividendo", "2025-09-05", 800, impagado=True)
datos = self.c.datos
f25 = fiscal.completo(datos.vivos("activos"), datos.vivos("movimientos"), datos.vivos("precios"), 2025, {"territorio": "comun"})["alquileres"][0]
self.assertEqual((f25["ingresos"], f25["gastos"]["dudoso_cobro"]), (1600.0, 800.0)) # el de marzo ya; el de septiembre, en 2026
self.mov(a["id"], "Dividendo", "2026-01-05", 800)
f26 = fiscal.completo(datos.vivos("activos"), datos.vivos("movimientos"), datos.vivos("precios"), 2026, {"territorio": "comun"})["alquileres"][0]
self.assertEqual(f26["gastos"]["dudoso_cobro"], 800.0)
vasco = fiscal.completo(datos.vivos("activos"), datos.vivos("movimientos"), datos.vivos("precios"), 2025, {"territorio": "bizkaia"})["alquileres"][0]
self.assertEqual(vasco["gastos"]["dudoso_cobro"], 0.0) # en la vivienda vasca solo cuentan los intereses
class TestFiscalidadDelAlquiler(unittest.TestCase):
"""Lo que hay que declarar del alquiler, territorio a territorio (fiscal/alquiler.py)."""
def setUp(self):
from cartera import fiscal
self.fiscal = fiscal
self.movs = []
def m(self, fecha, tipo, importe, aid="IN1", **extra):
self.movs.append({"id": f"m{len(self.movs) + 1}", "activo": aid, "tipo": tipo, "fecha": fecha, "importe": importe,
"orden": len(self.movs), **extra})
def inmueble(self, aid="IN1", **ficha):
return {"id": aid, "nombre": f"Inmueble {aid}", "tipo": "Inmueble", "valoracion": "total",
"inmueble": {"clase": "vivienda", "uso": "alquilado", "comunidad": "madrid", "porcentaje_construccion": 60,
"valor_catastral": 90000, **ficha}}
def con_datos(self):
self.m("2020-01-01", "Compra", 200000)
self.m("2025-06-01", "Dividendo", 12000)
self.m("2025-03-01", "Gasto", 3000, clase_gasto="intereses")
self.m("2025-04-01", "Gasto", 10000, clase_gasto="reparacion")
self.m("2025-05-01", "Gasto", 1000)
self.m("2026-06-01", "Dividendo", 12000)
def ficha(self, territorio, año, activos=None):
r = self.fiscal.completo(activos or [self.inmueble(reduccion=60)], self.movs, [], año, {"territorio": territorio})
return r, r["alquileres"][0] if r["alquileres"] else None
def test_comun_limita_intereses_y_reparaciones_y_arrastra_el_exceso(self):
self.con_datos()
_, f = self.ficha("comun", 2025)
# 13.000 de intereses y reparaciones, como mucho los 12.000 cobrados; amortización 3 % de 120.000
self.assertEqual((f["amortizacion"], f["deducido"], f["previo"], f["reduccion"], f["base"]),
(3600.0, 16600.0, -4600.0, 0.0, "general"))
self.assertEqual(f["pendiente_siguientes"], 1000.0)
_, f = self.ficha("comun", 2026)
self.assertEqual((f["deducido"], f["previo"], f["reduccion"], f["rendimiento"]), (4600.0, 7400.0, 4440.0, 2960.0))
def test_navarra_limita_todos_los_gastos_a_lo_cobrado(self):
self.con_datos()
_, f = self.ficha("navarra", 2025)
self.assertEqual((f["deducido"], f["previo"], f["pendiente_siguientes"]), (12000.0, 0.0, 5600.0))
_, f = self.ficha("navarra", 2026, [self.inmueble(contrato="tensionado")])
self.assertEqual((f["previo"], f["reduccion"]), (12000.0 - 3600 - 5600, (12000.0 - 3600 - 5600) * 0.5))
def test_en_bizkaia_la_vivienda_va_al_ahorro_con_su_bonificacion(self):
self.con_datos()
r, f = self.ficha("bizkaia", 2025)
self.assertEqual((f["bonificacion"], f["deducido"], f["rendimiento"], f["base"]), (3600.0, 3000.0, 5400.0, "ahorro"))
self.assertEqual(r["cuota"]["alquiler_ahorro"], 5400.0)
self.assertEqual(r["cuota"]["base"], 5400.0)
self.m("2024-06-01", "Dividendo", 10000)
self.assertEqual(self.ficha("bizkaia", 2024)[1]["bonificacion"], 2000.0) # antes de 2025, el 20 %
def test_temporada_y_garajes_van_a_la_general(self):
self.con_datos()
_, f = self.ficha("bizkaia", 2025, [self.inmueble(contrato="temporada")])
self.assertEqual((f["bonificacion"], f["base"]), (2400.0, "general"))
_, f = self.ficha("gipuzkoa", 2025, [self.inmueble(clase="garaje")])
self.assertEqual((f["modalidad"], f["base"], f["previo"]), ("no_vivienda", "general", 0.0)) # nunca negativo
def test_sin_parte_de_construccion_no_se_amortiza_y_se_avisa(self):
self.con_datos()
_, f = self.ficha("comun", 2026, [self.inmueble(porcentaje_construccion=None)])
self.assertEqual(f["amortizacion"], 0.0)
self.assertIn("construcción", f["avisos"][0])
def test_la_renta_imputada_de_un_vacio_en_territorio_comun(self):
self.m("2020-01-01", "Compra", 150000)
vacio = self.inmueble(uso="vacio", valor_catastral=100000)
r = self.fiscal.completo([vacio], self.movs, [], 2025, {"territorio": "comun"})
self.assertEqual((r["imputaciones"][0]["porcentaje"], r["imputaciones"][0]["renta"]), (2, 2000.0))
revisado = self.inmueble(uso="vacio", valor_catastral=100000, catastral_revisado=True)
self.assertEqual(self.fiscal.completo([revisado], self.movs, [], 2025, {"territorio": "comun"})["imputaciones"][0]["renta"], 1100.0)
self.assertEqual(self.fiscal.completo([vacio], self.movs, [], 2025, {"territorio": "navarra"})["imputaciones"], [])
def test_ponerse_al_dia_cuenta_el_alquiler_por_tu_tipo_marginal(self):
self.con_datos()
plan = self.fiscal.regularizacion([self.inmueble(reduccion=60)], self.movs, [], dt.date(2027, 10, 1),
{"territorio": "comun"}, {"2026": {"marginal": 30}})
f = next(x for x in plan["filas"] if x["ejercicio"] == 2026)
self.assertEqual((f["rendimiento"], f["general"]), (2960.0, 888.0))
def test_la_venta_da_una_ganancia_al_ahorro_restando_lo_amortizado(self):
self.con_datos()
self.m("2026-09-01", "Venta", 260000)
r = self.fiscal.completo([self.inmueble(reduccion=60)], self.movs, [], 2026, {"territorio": "comun"})
v = r["ventas_inmuebles"][0]
# solo los años alquilados: 2025 entero (3.600) y 2026 hasta la venta del 1 de septiembre (244 días)
self.assertEqual(v["amortizacion"], round(3600 + 3600 * 244 / 365, 2))
self.assertEqual(v["valor_adquisicion"], round(200000 - v["amortizacion"], 2))
self.assertEqual(v["ganancia"], round(260000 - v["valor_adquisicion"], 2))
self.assertEqual(r["cuota"]["ganancias"], v["ganancia"])
self.assertEqual(r["fuera"], [])
def test_en_bizkaia_el_coste_se_actualiza_y_no_hay_amortizacion_en_vivienda(self):
self.m("2020-01-01", "Compra", 200000)
self.m("2026-09-01", "Venta", 260000)
v = self.fiscal.completo([self.inmueble()], self.movs, [], 2026, {"territorio": "bizkaia"})["ventas_inmuebles"][0]
self.assertEqual((v["actualizado"], v["amortizacion"], v["ganancia"]), (249800.0, 0.0, 10200.0)) # 200.000 × 1,249
if __name__ == "__main__":
unittest.main()