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