""" Dialogs for Portfolio Manager. Contains dialogs for Add Holding, Buy/Sell/Dividend transactions. """ import logging from datetime import datetime from PyQt6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QComboBox, QDoubleSpinBox, QPushButton, QMessageBox, QLabel, QDateEdit, QTextEdit ) from PyQt6.QtCore import QDate logger = logging.getLogger(__name__) class AddHoldingDialog(QDialog): """Dialog for adding a new holding to the portfolio.""" def __init__(self, parent): """ Initialize Add Holding dialog. Args: parent: MainWindow instance """ super().__init__(parent) self.parent = parent self.setWindowTitle("Add New Holding") self.setMinimumWidth(400) self.init_ui() def init_ui(self): """Initialize user interface.""" layout = QVBoxLayout(self) # Form layout form_layout = QFormLayout() # Ticker self.txt_ticker = QLineEdit() self.txt_ticker.setPlaceholderText("e.g., VWCE.MI") form_layout.addRow("Ticker:", self.txt_ticker) # Name self.txt_name = QLineEdit() self.txt_name.setPlaceholderText("e.g., Vanguard FTSE All-World") form_layout.addRow("Name:", self.txt_name) # Type self.combo_type = QComboBox() self.combo_type.addItems(["ETF", "Stock", "Cash"]) form_layout.addRow("Asset Type:", self.combo_type) # Quantity self.spin_quantity = QDoubleSpinBox() self.spin_quantity.setDecimals(2) self.spin_quantity.setRange(0, 1000000) self.spin_quantity.setSuffix(" shares") form_layout.addRow("Quantity:", self.spin_quantity) # Average Price self.spin_avg_price = QDoubleSpinBox() self.spin_avg_price.setDecimals(2) self.spin_avg_price.setRange(0, 1000000) self.spin_avg_price.setPrefix("€ ") form_layout.addRow("Avg Price:", self.spin_avg_price) layout.addLayout(form_layout) # Buttons btn_layout = QHBoxLayout() btn_cancel = QPushButton("Cancel") btn_cancel.clicked.connect(self.reject) btn_add = QPushButton("Add Holding") btn_add.clicked.connect(self.accept_dialog) btn_add.setDefault(True) btn_layout.addStretch() btn_layout.addWidget(btn_cancel) btn_layout.addWidget(btn_add) layout.addLayout(btn_layout) def accept_dialog(self): """Validate and accept dialog.""" # Validate inputs ticker = self.txt_ticker.text().strip().upper() name = self.txt_name.text().strip() asset_type = self.combo_type.currentText() quantity = self.spin_quantity.value() avg_price = self.spin_avg_price.value() if not ticker: QMessageBox.warning(self, "Validation Error", "Ticker is required") return if not name: QMessageBox.warning(self, "Validation Error", "Name is required") return if quantity <= 0: QMessageBox.warning(self, "Validation Error", "Quantity must be greater than 0") return if avg_price <= 0: QMessageBox.warning(self, "Validation Error", "Average price must be greater than 0") return # Add holding try: self.parent.portfolio.add_holding( ticker=ticker, name=name, asset_type=asset_type, quantity=quantity, avg_price=avg_price ) logger.info(f"Added holding: {ticker} ({quantity} @ {avg_price})") self.accept() except Exception as e: logger.error(f"Error adding holding: {e}", exc_info=True) QMessageBox.critical( self, "Error", f"Failed to add holding:\n{str(e)}" ) class BuyTransactionDialog(QDialog): """Dialog for recording a BUY transaction.""" def __init__(self, parent): """ Initialize Buy Transaction dialog. Args: parent: MainWindow instance """ super().__init__(parent) self.parent = parent self.setWindowTitle("Buy Transaction") self.setMinimumWidth(400) self.init_ui() def init_ui(self): """Initialize user interface.""" layout = QVBoxLayout(self) # Form layout form_layout = QFormLayout() # Ticker dropdown self.combo_ticker = QComboBox() self.combo_ticker.setEditable(True) self.load_tickers() form_layout.addRow("Ticker:", self.combo_ticker) # Quantity self.spin_quantity = QDoubleSpinBox() self.spin_quantity.setDecimals(2) self.spin_quantity.setRange(0, 1000000) self.spin_quantity.setSuffix(" shares") self.spin_quantity.valueChanged.connect(self.update_total) form_layout.addRow("Quantity:", self.spin_quantity) # Price self.spin_price = QDoubleSpinBox() self.spin_price.setDecimals(2) self.spin_price.setRange(0, 1000000) self.spin_price.setPrefix("€ ") self.spin_price.valueChanged.connect(self.update_total) form_layout.addRow("Price:", self.spin_price) # Date self.date_edit = QDateEdit() self.date_edit.setDate(QDate.currentDate()) self.date_edit.setCalendarPopup(True) form_layout.addRow("Date:", self.date_edit) # Notes self.txt_notes = QLineEdit() self.txt_notes.setPlaceholderText("Optional notes...") form_layout.addRow("Notes:", self.txt_notes) # Total (calculated) self.lbl_total = QLabel("€ 0.00") self.lbl_total.setStyleSheet("font-weight: bold; font-size: 14px;") form_layout.addRow("Total:", self.lbl_total) layout.addLayout(form_layout) # Buttons btn_layout = QHBoxLayout() btn_cancel = QPushButton("Cancel") btn_cancel.clicked.connect(self.reject) btn_buy = QPushButton("Buy") btn_buy.clicked.connect(self.accept_dialog) btn_buy.setDefault(True) btn_layout.addStretch() btn_layout.addWidget(btn_cancel) btn_layout.addWidget(btn_buy) layout.addLayout(btn_layout) def load_tickers(self): """Load existing tickers into dropdown.""" tickers = self.parent.db_manager.get_all_tickers() self.combo_ticker.addItems(tickers) def update_total(self): """Update total amount label.""" quantity = self.spin_quantity.value() price = self.spin_price.value() total = quantity * price self.lbl_total.setText(f"€ {total:,.2f}") def accept_dialog(self): """Validate and accept dialog.""" ticker = self.combo_ticker.currentText().strip().upper() quantity = self.spin_quantity.value() price = self.spin_price.value() date = self.date_edit.date().toString("yyyy-MM-dd") notes = self.txt_notes.text().strip() if not ticker: QMessageBox.warning(self, "Validation Error", "Ticker is required") return if quantity <= 0: QMessageBox.warning(self, "Validation Error", "Quantity must be greater than 0") return if price <= 0: QMessageBox.warning(self, "Validation Error", "Price must be greater than 0") return # Process buy transaction try: self.parent.transaction_manager.process_buy( ticker=ticker, quantity=quantity, price=price, date=date, notes=notes ) logger.info(f"Buy transaction processed: {ticker} {quantity} @ {price}") self.accept() except Exception as e: logger.error(f"Error processing buy: {e}", exc_info=True) QMessageBox.critical( self, "Error", f"Failed to process buy transaction:\n{str(e)}" ) class SellTransactionDialog(QDialog): """Dialog for recording a SELL transaction.""" def __init__(self, parent): """ Initialize Sell Transaction dialog. Args: parent: MainWindow instance """ super().__init__(parent) self.parent = parent self.setWindowTitle("Sell Transaction") self.setMinimumWidth(400) self.init_ui() def init_ui(self): """Initialize user interface.""" layout = QVBoxLayout(self) # Form layout form_layout = QFormLayout() # Ticker dropdown (only holdings with quantity > 0) self.combo_ticker = QComboBox() self.combo_ticker.currentIndexChanged.connect(self.on_ticker_changed) self.load_tickers() form_layout.addRow("Ticker:", self.combo_ticker) # Available quantity label self.lbl_available = QLabel("Available: 0 shares") form_layout.addRow("", self.lbl_available) # Quantity self.spin_quantity = QDoubleSpinBox() self.spin_quantity.setDecimals(2) self.spin_quantity.setRange(0, 1000000) self.spin_quantity.setSuffix(" shares") self.spin_quantity.valueChanged.connect(self.update_total) form_layout.addRow("Quantity:", self.spin_quantity) # Price self.spin_price = QDoubleSpinBox() self.spin_price.setDecimals(2) self.spin_price.setRange(0, 1000000) self.spin_price.setPrefix("€ ") self.spin_price.valueChanged.connect(self.update_total) form_layout.addRow("Price:", self.spin_price) # Date self.date_edit = QDateEdit() self.date_edit.setDate(QDate.currentDate()) self.date_edit.setCalendarPopup(True) form_layout.addRow("Date:", self.date_edit) # Notes self.txt_notes = QLineEdit() self.txt_notes.setPlaceholderText("Optional notes...") form_layout.addRow("Notes:", self.txt_notes) # Total (calculated) self.lbl_total = QLabel("€ 0.00") self.lbl_total.setStyleSheet("font-weight: bold; font-size: 14px;") form_layout.addRow("Total:", self.lbl_total) layout.addLayout(form_layout) # Buttons btn_layout = QHBoxLayout() btn_cancel = QPushButton("Cancel") btn_cancel.clicked.connect(self.reject) btn_sell = QPushButton("Sell") btn_sell.clicked.connect(self.accept_dialog) btn_sell.setDefault(True) btn_layout.addStretch() btn_layout.addWidget(btn_cancel) btn_layout.addWidget(btn_sell) layout.addLayout(btn_layout) # Trigger initial ticker changed self.on_ticker_changed() def load_tickers(self): """Load tickers with quantity > 0.""" holdings = self.parent.portfolio.holdings tickers = [h.ticker for h in holdings if h.quantity > 0 and h.asset_type != 'Cash'] self.combo_ticker.addItems(tickers) def on_ticker_changed(self): """Update available quantity when ticker changes.""" ticker = self.combo_ticker.currentText() if ticker: holding = self.parent.portfolio.get_holding(ticker) if holding: self.lbl_available.setText(f"Available: {holding.quantity:.0f} shares") self.spin_quantity.setMaximum(holding.quantity) def update_total(self): """Update total amount label.""" quantity = self.spin_quantity.value() price = self.spin_price.value() total = quantity * price self.lbl_total.setText(f"€ {total:,.2f}") def accept_dialog(self): """Validate and accept dialog.""" ticker = self.combo_ticker.currentText() quantity = self.spin_quantity.value() price = self.spin_price.value() date = self.date_edit.date().toString("yyyy-MM-dd") notes = self.txt_notes.text().strip() if not ticker: QMessageBox.warning(self, "Validation Error", "Ticker is required") return if quantity <= 0: QMessageBox.warning(self, "Validation Error", "Quantity must be greater than 0") return if price <= 0: QMessageBox.warning(self, "Validation Error", "Price must be greater than 0") return # Process sell transaction try: pnl_amount, pnl_percent = self.parent.transaction_manager.process_sell( ticker=ticker, quantity=quantity, price=price, date=date, notes=notes ) # Show realized P&L QMessageBox.information( self, "Sale Complete", f"Sold {quantity:.0f} shares of {ticker}\n\n" f"Realized P&L: {pnl_amount:+,.2f} € ({pnl_percent:+.1f}%)" ) logger.info(f"Sell transaction processed: {ticker} {quantity} @ {price} (P&L: {pnl_amount:+.2f})") self.accept() except ValueError as e: QMessageBox.warning(self, "Validation Error", str(e)) except Exception as e: logger.error(f"Error processing sell: {e}", exc_info=True) QMessageBox.critical( self, "Error", f"Failed to process sell transaction:\n{str(e)}" ) class DividendDialog(QDialog): """Dialog for recording a DIVIDEND transaction.""" def __init__(self, parent): """ Initialize Dividend dialog. Args: parent: MainWindow instance """ super().__init__(parent) self.parent = parent self.setWindowTitle("Dividend") self.setMinimumWidth(400) self.init_ui() def init_ui(self): """Initialize user interface.""" layout = QVBoxLayout(self) # Form layout form_layout = QFormLayout() # Ticker dropdown self.combo_ticker = QComboBox() self.load_tickers() form_layout.addRow("Ticker:", self.combo_ticker) # Amount self.spin_amount = QDoubleSpinBox() self.spin_amount.setDecimals(2) self.spin_amount.setRange(0, 1000000) self.spin_amount.setPrefix("€ ") form_layout.addRow("Amount:", self.spin_amount) # Date self.date_edit = QDateEdit() self.date_edit.setDate(QDate.currentDate()) self.date_edit.setCalendarPopup(True) form_layout.addRow("Date:", self.date_edit) # Notes self.txt_notes = QLineEdit() self.txt_notes.setPlaceholderText("e.g., Q4 2025") form_layout.addRow("Notes:", self.txt_notes) layout.addLayout(form_layout) # Buttons btn_layout = QHBoxLayout() btn_cancel = QPushButton("Cancel") btn_cancel.clicked.connect(self.reject) btn_add = QPushButton("Add Dividend") btn_add.clicked.connect(self.accept_dialog) btn_add.setDefault(True) btn_layout.addStretch() btn_layout.addWidget(btn_cancel) btn_layout.addWidget(btn_add) layout.addLayout(btn_layout) def load_tickers(self): """Load existing tickers.""" tickers = self.parent.db_manager.get_all_tickers() self.combo_ticker.addItems(tickers) def accept_dialog(self): """Validate and accept dialog.""" ticker = self.combo_ticker.currentText() amount = self.spin_amount.value() date = self.date_edit.date().toString("yyyy-MM-dd") notes = self.txt_notes.text().strip() if not ticker: QMessageBox.warning(self, "Validation Error", "Ticker is required") return if amount <= 0: QMessageBox.warning(self, "Validation Error", "Amount must be greater than 0") return # Process dividend try: self.parent.transaction_manager.process_dividend( ticker=ticker, amount=amount, date=date, notes=notes ) logger.info(f"Dividend processed: {ticker} €{amount} ({notes})") self.accept() except Exception as e: logger.error(f"Error processing dividend: {e}", exc_info=True) QMessageBox.critical( self, "Error", f"Failed to process dividend:\n{str(e)}" )