327 lines
18 KiB
Python
327 lines
18 KiB
Python
"""
|
||
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()
|