""" Pruebas de la renta fija (cartera/rentafija.py): el valor que se calcula solo y lo que tributa de Letras, bonos y depósitos. python3 -m unittest pruebas.test_rentafija -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 fiscal, rentafija # noqa: E402 from cartera.datos import Datos, escribir # noqa: E402 from cartera.operaciones import Cartera, ErrorOperacion # noqa: E402 from cartera.vista import resumen # noqa: E402 def activo(**ficha): return {"id": "RF1", "nombre": "Letra a 12 meses", "tipo": "RentaFija", "valoracion": "total", "renta_fija": {"clase": "letra", "nominal": 10000, "vencimiento": "2026-11-06", "pagos": 0, **ficha}} def mov(fecha, tipo, importe, cantidad=None, n=[0], **extra): n[0] += 1 return {"id": f"m{n[0]}", "activo": "RF1", "tipo": tipo, "fecha": fecha, "importe": importe, "cantidad": cantidad, **extra} class TestValor(unittest.TestCase): def test_una_letra_va_de_lo_pagado_al_nominal(self): movs = [mov("2025-11-10", "Compra", 9750, 10000)] a = activo() self.assertEqual(rentafija.valor_en(a, movs, dt.date(2025, 11, 10)), 9750.0) mitad = dt.date(2025, 11, 10) + dt.timedelta(days=(dt.date(2026, 11, 6) - dt.date(2025, 11, 10)).days // 2) self.assertAlmostEqual(rentafija.valor_en(a, movs, mitad), 9875, delta=1) self.assertEqual(rentafija.valor_en(a, movs, dt.date(2026, 11, 6)), 10000.0) self.assertEqual(rentafija.valor_en(a, movs + [mov("2026-11-06", "Reembolso", 10000, 10000)], dt.date(2026, 11, 30)), 0.0) def test_un_deposito_suma_los_intereses_que_se_van_ganando(self): a = activo(clase="deposito", interes=3.0, vencimiento="2026-06-30", pagos=0) movs = [mov("2025-06-30", "Compra", 10000, 10000)] self.assertAlmostEqual(rentafija.valor_en(a, movs, dt.date(2025, 12, 31)), 10000 + 10000 * 0.03 * 184 / 365, delta=0.01) def test_un_bono_con_cupon_anual_reinicia_los_intereses_en_cada_pago(self): a = activo(clase="bono", interes=4.0, vencimiento="2030-04-30", pagos=1) movs = [mov("2025-04-30", "Compra", 10000, 10000)] self.assertEqual(rentafija.ultimo_pago(dt.date(2025, 4, 30), dt.date(2030, 4, 30), 1, dt.date(2026, 7, 1)), dt.date(2026, 4, 30)) self.assertAlmostEqual(rentafija.valor_en(a, movs, dt.date(2026, 7, 31)), 10000 + 10000 * 0.04 * 92 / 365, delta=0.01) def test_hay_un_valor_cada_fin_de_mes_hasta_hoy(self): e = rentafija.estimaciones(activo(), [mov("2025-11-10", "Compra", 9750, 10000)], dt.date(2026, 3, 15)) self.assertEqual([x["fecha"] for x in e], ["2025-11-30", "2025-12-31", "2026-01-31", "2026-02-28"]) class TestFiscal(unittest.TestCase): def test_la_ganancia_de_una_letra_tributa_al_vencer_como_rendimiento(self): movs = [mov("2025-11-10", "Compra", 9750, 10000), mov("2026-11-06", "Reembolso", 10000, 10000)] r = fiscal.completo([activo()], movs, [], 2026, {"territorio": "comun"}) self.assertEqual([(x["concepto"], x["bruto"]) for x in r["rendimientos"]], [("Renta fija: devolución o venta", 250.0)]) self.assertEqual(r["cuota"]["rendimientos"], 250.0) self.assertEqual(r["fuera"], []) def test_vender_antes_y_por_menos_da_un_rendimiento_negativo(self): movs = [mov("2025-01-10", "Compra", 10200, 10000), mov("2025-08-01", "Reembolso", 5000, 5000)] r = fiscal.completo([activo(clase="bono", interes=3, pagos=1)], movs, [], 2025, {"territorio": "comun"}) self.assertEqual(r["rendimientos"][0]["bruto"], -100.0) # la mitad costó 5.100 def test_los_cupones_son_rendimientos_con_su_retencion(self): movs = [mov("2025-01-10", "Compra", 10000, 10000), mov("2025-07-10", "Dividendo", 162, retencion=38)] r = fiscal.completo([activo(clase="bono", interes=4, pagos=2)], movs, [], 2025, {"territorio": "bizkaia"}) self.assertEqual((r["rendimientos"][0]["concepto"], r["rendimientos"][0]["bruto"], r["cuota"]["retenciones"]), ("Cupones e intereses de renta fija", 200.0, 38.0)) class TestEnLaCartera(unittest.TestCase): def setUp(self): t = tempfile.TemporaryDirectory() self.addCleanup(t.cleanup) carpeta = Path(t.name) / "datos" escribir(carpeta, Datos([], [], [], [])) self.c = Cartera(carpeta) self.addCleanup(self.c.cerrar) def test_alta_compra_valor_solo_y_aviso_al_vencer(self): a = self.c.guardar_activo({"tipo": "RentaFija", "nombre": "Letra", "renta_fija": {"clase": "letra", "nominal": 10000, "vencimiento": "2025-12-05", "pagos": 4}}) self.assertEqual((a["id"][:2], a["renta_fija"]["pagos"], a["valoracion"]), ("RF", 0, "total")) # una Letra no paga cupones self.c.guardar_movimiento({"activo": a["id"], "tipo": "Compra", "fecha": "2025-06-06", "importe": 9850, "cantidad": 10000}) devengo = [r for r in self.c.datos.vivos("precios") if r.get("fuente") == "devengo"] self.assertGreaterEqual(len(devengo), 6) r = resumen(self.c.datos, dt.date(2026, 1, 10)) self.assertTrue(any(x["tipo"] == "vencido" for x in r["avisos"])) def test_vender_parte_guarda_el_nominal_y_sin_el_toma_el_de_la_ficha(self): a = self.c.guardar_activo({"tipo": "RentaFija", "nombre": "Bono", "renta_fija": {"clase": "bono", "nominal": 10000, "interes": 3, "vencimiento": "2030-01-10", "pagos": 1}}) self.c.guardar_movimiento({"activo": a["id"], "tipo": "Compra", "fecha": "2025-01-10", "importe": 9900}) v = self.c.guardar_movimiento({"activo": a["id"], "tipo": "Reembolso", "fecha": "2025-06-10", "importe": 5100, "cantidad": 5000}) self.assertEqual(v["cantidad"], 5000) movs = [m for m in self.c.datos.vivos("movimientos") if m["activo"] == a["id"]] self.assertEqual(rentafija.coste_vivo(movs, "2025-12-31", 10000), (4950.0, 5000.0)) def test_sin_vencimiento_no_se_acepta(self): with self.assertRaises(ErrorOperacion): self.c.guardar_activo({"tipo": "RentaFija", "nombre": "Bono", "renta_fija": {"clase": "bono", "pagos": 1}}) class TestCobrosAutomaticos(unittest.TestCase): def test_un_bono_semestral_genera_sus_cupones_y_la_devolucion(self): a = activo(clase="bono", interes=4.0, vencimiento="2026-06-30", pagos=2) movs = [mov("2025-01-15", "Compra", 10000, 10000)] c = rentafija.calendario(a, movs, dt.date(2026, 10, 1)) self.assertEqual([(x["fecha"], x["tipo"], x.get("retencion")) for x in c], [("2025-06-30", "Dividendo", 38.0), ("2025-12-30", "Dividendo", 38.0), ("2026-06-30", "Dividendo", 38.0), ("2026-06-30", "Reembolso", None)]) self.assertEqual((c[0]["importe"], c[-1]["importe"]), (162.0, 10000.0)) self.assertTrue(all(x["estimado"] for x in c)) def test_no_repite_lo_apuntado_a_mano_y_se_para_si_se_vende(self): a = activo(clase="bono", interes=4.0, vencimiento="2027-06-30", pagos=1) movs = [mov("2025-01-15", "Compra", 10000, 10000), mov("2025-07-02", "Dividendo", 324, retencion=76), mov("2026-01-20", "Reembolso", 10100, 10000)] self.assertEqual(rentafija.calendario(a, movs, dt.date(2027, 12, 1)), []) def test_un_deposito_cobra_los_intereses_al_vencer_y_una_letra_solo_el_nominal(self): d = rentafija.calendario(activo(clase="deposito", interes=3.0, vencimiento="2026-01-15", pagos=0), [mov("2025-01-15", "Compra", 10000, 10000)], dt.date(2026, 3, 1)) self.assertEqual([(x["tipo"], x["importe"]) for x in d], [("Dividendo", 243.0), ("Reembolso", 10000.0)]) letra = rentafija.calendario(activo(), [mov("2025-11-10", "Compra", 9750, 10000)], dt.date(2026, 12, 1)) self.assertEqual([(x["tipo"], x["importe"]) for x in letra], [("Reembolso", 10000.0)]) def test_en_la_cartera_se_apuntan_solos_y_lo_borrado_no_vuelve(self): t = tempfile.TemporaryDirectory() self.addCleanup(t.cleanup) carpeta = Path(t.name) / "datos" escribir(carpeta, Datos([], [], [], [])) c = Cartera(carpeta) self.addCleanup(c.cerrar) a = c.guardar_activo({"tipo": "RentaFija", "nombre": "Letra", "renta_fija": {"clase": "letra", "nominal": 10000, "vencimiento": "2025-12-05"}}) c.guardar_movimiento({"activo": a["id"], "tipo": "Compra", "fecha": "2025-06-06", "importe": 9850, "cantidad": 10000}) auto = [m for m in c.datos.vivos("movimientos") if m["id"].startswith("rf-")] self.assertEqual([(m["tipo"], m["fecha"]) for m in auto], [("Reembolso", "2025-12-05")]) c.borrar_movimiento(auto[0]["id"]) c._guardar() self.assertEqual([m for m in c.datos.vivos("movimientos") if m["id"].startswith("rf-")], []) if __name__ == "__main__": unittest.main()