#!/usr/bin/env python3 """ exportar_xlsx.py — Los datos de la cartera en una hoja de cálculo. python3 -m cartera.exportar_xlsx datos/ cartera.xlsx [--fecha AAAA-MM-DD] Genera un libro con tres hojas —Cartera (lo mismo que ves en el resumen de la aplicación), Movimientos y Precios— con valores, sin fórmulas y sin macros: se abre igual en LibreOffice, Excel o Google Sheets, y sirve para mirar los números fuera del programa, para mandárselos a quien te lleve los impuestos o para guardarlos en un formato que cualquiera entiende dentro de veinte años. Es una foto de salida, no un formato de trabajo: lo que se edite aquí no vuelve al programa. Los datos de verdad son los ficheros de texto de la carpeta de datos, que se leen con cualquier editor. No hace peticiones de red. No sobrescribe un fichero existente sin --forzar. Necesita openpyxl (`pip install openpyxl`); todo lo demás funciona sin él. """ from __future__ import annotations import argparse import datetime as dt import sys from pathlib import Path from .datos import Datos, leer from .vista import resumen GRIS = "FFEFEFEF" # Los porcentajes salen del programa en tanto por uno (0,60 = 60 %), que es justo # lo que espera el formato «0.00%» de la hoja. EUROS, UNIDADES, PCT = "#,##0.00", "#,##0.0000", "0.00%" COLUMNAS_CARTERA = [ ("Activo", 30, None), ("Tipo", 15, None), ("Cantidad", 14, UNIDADES), ("Precio", 12, UNIDADES), ("Valor", 14, EUROS), ("Coste", 14, EUROS), ("Ganancia", 14, EUROS), ("Rentabilidad", 12, PCT), ("TWR", 10, PCT), ("TIR", 10, PCT), ("Año", 10, PCT), ("Peso", 10, PCT), ("Dividendos", 12, EUROS), ("Desde", 11, None), ] COLUMNAS_MOVIMIENTOS = [ ("Fecha", 12, "DD/MM/YYYY"), ("Activo", 26, None), ("Tipo", 17, None), ("Cantidad", 14, UNIDADES), ("Precio", 12, UNIDADES), ("Importe", 13, EUROS), ("Comisiones", 11, EUROS), ("Coste traspaso", 13, EUROS), ("Retención", 11, EUROS), ("Moneda", 8, None), ("Actividad", 16, None), ("Notas", 40, None), ] COLUMNAS_PRECIOS = [ ("Fecha", 12, "DD/MM/YYYY"), ("Activo", 26, None), ("Precio", 14, UNIDADES), ("Valor total", 14, EUROS), ("Fuente", 18, None), ] def _openpyxl(): try: import openpyxl # noqa: F401 except ImportError: raise RuntimeError("Para exportar a Excel hace falta openpyxl: pip install openpyxl") from None def _fecha(v): try: return dt.date.fromisoformat(v) except (TypeError, ValueError): return v class _Hoja: """Una hoja con sus columnas; escribe filas de datos, títulos y totales.""" def __init__(self, wb, titulo: str, columnas: list[tuple]): from openpyxl.styles import Alignment, Font, PatternFill from openpyxl.utils import get_column_letter self.ws = wb.create_sheet(titulo) self.columnas = columnas self.fila = 1 for i, (nombre, ancho, _) in enumerate(columnas, 1): c = self.ws.cell(1, i, nombre) c.font = Font(bold=True) c.fill = PatternFill("solid", fgColor=GRIS) c.alignment = Alignment(horizontal="center") self.ws.column_dimensions[get_column_letter(i)].width = ancho self.ws.freeze_panes = "A2" self.fila = 2 def datos(self, valores: list, negrita: bool = False) -> None: from openpyxl.styles import Font for i, (v, (_, _, formato)) in enumerate(zip(valores, self.columnas), 1): c = self.ws.cell(self.fila, i, v) if formato and isinstance(v, (int, float)) and not isinstance(v, bool): c.number_format = formato elif formato == "DD/MM/YYYY" and isinstance(v, dt.date): c.number_format = formato if negrita: c.font = Font(bold=True) self.fila += 1 def titulo(self, texto: str) -> None: from openpyxl.styles import Font self.fila += 1 c = self.ws.cell(self.fila, 1, texto) c.font = Font(bold=True, size=12) self.fila += 1 def pareja(self, etiqueta: str, valor, formato: str = EUROS) -> None: self.ws.cell(self.fila, 1, etiqueta) c = self.ws.cell(self.fila, 2, valor) if isinstance(valor, (int, float)) and not isinstance(valor, bool): c.number_format = formato self.fila += 1 def _fila_activo(f: dict) -> list: return [f["nombre"], f["tipo"], f["cantidad"], f["precio"], f["valor"], f["coste"], f["ganancia"], f["rentabilidad"], f["twr"], f["tir"], f["ytd"], f.get("peso"), f["dividendos"], f.get("fecha_inicio")] def exportar(datos: Datos, destino: Path, hoy: dt.date) -> None: """Escribe el libro con la cartera a la fecha «hoy».""" _openpyxl() from openpyxl import Workbook r = resumen(datos, hoy) g = r["global"] nombres = {a["id"]: (a.get("nombre") or a["id"]) for a in datos.vivos("activos")} wb = Workbook() wb.remove(wb.active) # ── Cartera: lo mismo que el resumen de la aplicación ──────────────────── h = _Hoja(wb, "Cartera", COLUMNAS_CARTERA) for f in r["abiertos"]: h.datos(_fila_activo(f)) h.datos(["TOTAL", None, None, None, g["valor"], g["coste"], g["ganancia_latente"], g["rentabilidad_latente"], None, g["tir"], g["ytd"], None, g["dividendos"], None], negrita=True) if r["cerrados"]: h.titulo("Inversiones cerradas") for f in r["cerrados"]: h.datos([f["nombre"], f["tipo"], None, None, None, None, f["ganancia"], f.get("rentabilidad"), None, f["tir"], None, None, f["dividendos"], f.get("fecha_cierre")]) if r["liquidez"]: h.titulo("Cuentas") for f in r["liquidez"]: h.datos([f["nombre"], f["tipo"], None, None, f["saldo"], None, None, None, None, None, None, None, f.get("intereses"), f.get("fecha_inicio")]) if r["deudas"]: h.titulo("Préstamos recibidos") for f in r["deudas"]: h.datos([f["nombre"], f["tipo"], None, None, -f["pendiente"], None, None, (f.get("tin") or 0) / 100, None, None, None, None, None, f.get("fin")]) h.titulo(f"Resumen a {hoy.strftime('%d/%m/%Y')}") h.pareja("Valor de las inversiones", g["valor"]) h.pareja("Dinero en cuentas", g["liquidez"]) h.pareja("Patrimonio", g["patrimonio"]) if g["deuda"]: h.pareja("Deuda pendiente", -g["deuda"]) h.pareja("Patrimonio neto", g["patrimonio_neto"]) h.pareja("Ganancia latente", g["ganancia_latente"]) h.pareja("Ganancia realizada", g["ganancia_realizada"]) h.pareja("Dividendos e intereses cobrados", g["dividendos"]) h.pareja("TIR", g["tir"], PCT) h.pareja("ROI", g["roi"], PCT) h.fila += 1 h.ws.cell(h.fila, 1, "Exportado desde la cartera; los datos de verdad están en la carpeta de datos.") # ── Movimientos ────────────────────────────────────────────────────────── h = _Hoja(wb, "Movimientos", COLUMNAS_MOVIMIENTOS) movs = sorted(datos.vivos("movimientos"), key=lambda m: (m["fecha"], m.get("orden") or 0, m["id"])) for m in movs: h.datos([_fecha(m["fecha"]), nombres.get(m["activo"], m["activo"]), m["tipo"], m.get("cantidad"), m.get("precio"), m.get("importe"), m.get("comisiones"), m.get("coste_traspaso"), m.get("retencion"), m.get("moneda") or "EUR", m.get("actividad"), m.get("notas")]) h.ws.auto_filter.ref = f"A1:L{max(len(movs) + 1, 2)}" # ── Precios ────────────────────────────────────────────────────────────── h = _Hoja(wb, "Precios", COLUMNAS_PRECIOS) for p in sorted(datos.vivos("precios"), key=lambda p: (p["fecha"], p["activo"])): h.datos([_fecha(p["fecha"]), nombres.get(p["activo"], p["activo"]), p.get("precio"), p.get("valor_total"), p.get("fuente")]) destino = Path(destino) destino.parent.mkdir(parents=True, exist_ok=True) wb.save(destino) def main() -> int: p = argparse.ArgumentParser(description="Datos de la cartera → hoja de cálculo") p.add_argument("datos") p.add_argument("xlsx") p.add_argument("--fecha", help="fecha de la foto (por defecto, hoy)") p.add_argument("--forzar", action="store_true") args = p.parse_args() destino = Path(args.xlsx) if destino.exists() and not args.forzar: print(f"ERROR: {destino} ya existe. Usa --forzar para sobrescribir.") return 1 hoy = dt.date.fromisoformat(args.fecha) if args.fecha else dt.date.today() exportar(leer(Path(args.datos)), destino, hoy) print(f"✓ Exportado {args.datos} → {destino} (cartera a {hoy.isoformat()})") return 0 if __name__ == "__main__": sys.exit(main())