caudal/programa/pruebas/test_rentafija.py
esetheyers 7e18576f86 Caudal 0.2.0
Taxes for the whole of Spain (common territory, Navarre, Álava, Bizkaia and Gipuzkoa); flats, garages and land valued on their own; Treasury bills, bonds and deposits priced on BME; broker accounts; a guided start; a page per investment with its chart and the tax of selling today; net worth at the top; undo when deleting; Woodpecker CI for the tests and the Linux build.
2026-10-15 16:49:15 -05:00

155 lines
9.1 KiB
Python

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