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.
155 lines
9.1 KiB
Python
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()
|