""" Transactions Tab - Transaction Log. Displays transaction history with filters and action buttons. """ import logging from PyQt6.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QTableWidget, QTableWidgetItem, QLabel, QHeaderView, QMessageBox, QMenu ) from PyQt6.QtCore import Qt from PyQt6.QtGui import QColor, QBrush from src.utils.formatters import format_currency, format_date from src.gui.dialogs import BuyTransactionDialog, SellTransactionDialog, DividendDialog logger = logging.getLogger(__name__) class TransactionsTab(QWidget): """Transaction log tab with filters.""" def __init__(self, parent): """ Initialize transactions tab. Args: parent: MainWindow instance """ super().__init__() self.parent = parent self.init_ui() self.load_data() def init_ui(self): """Initialize user interface.""" layout = QVBoxLayout(self) # Action buttons section btn_layout = QHBoxLayout() self.btn_buy = QPushButton("💰 Buy") self.btn_buy.clicked.connect(self.on_buy_clicked) self.btn_sell = QPushButton("💸 Sell") self.btn_sell.clicked.connect(self.on_sell_clicked) self.btn_dividend = QPushButton("💵 Dividend") self.btn_dividend.clicked.connect(self.on_dividend_clicked) btn_layout.addWidget(self.btn_buy) btn_layout.addWidget(self.btn_sell) btn_layout.addWidget(self.btn_dividend) # Filters btn_layout.addStretch() btn_layout.addWidget(QLabel("Type:")) self.combo_type_filter = QComboBox() self.combo_type_filter.addItems(["All", "BUY", "SELL", "DIVIDEND"]) self.combo_type_filter.currentTextChanged.connect(self.apply_filters) btn_layout.addWidget(self.combo_type_filter) btn_layout.addWidget(QLabel("Ticker:")) self.combo_ticker_filter = QComboBox() self.combo_ticker_filter.addItem("All") self.combo_ticker_filter.currentTextChanged.connect(self.apply_filters) btn_layout.addWidget(self.combo_ticker_filter) layout.addLayout(btn_layout) # Transactions table self.table = QTableWidget() self.table.setColumnCount(7) self.table.setHorizontalHeaderLabels([ "Date", "Ticker", "Type", "Quantity", "Price €", "Amount €", "Notes" ]) # Configure table self.table.setAlternatingRowColors(True) self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) self.table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) self.table.setSortingEnabled(True) # Set column widths header = self.table.horizontalHeader() header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # Date header.setSectionResizeMode(1, QHeaderView.ResizeMode.ResizeToContents) # Ticker header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents) # Type header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents) # Quantity header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents) # Price header.setSectionResizeMode(5, QHeaderView.ResizeMode.ResizeToContents) # Amount header.setSectionResizeMode(6, QHeaderView.ResizeMode.Stretch) # Notes # Context menu self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) self.table.customContextMenuRequested.connect(self.show_context_menu) layout.addWidget(self.table) # Summary label self.lbl_summary = QLabel("Total transactions: 0") layout.addWidget(self.lbl_summary) def load_data(self, tx_type=None, ticker=None): """ Load and display transactions. Args: tx_type: Filter by transaction type (None for all) ticker: Filter by ticker (None for all) """ logger.debug(f"Loading transactions (type={tx_type}, ticker={ticker})...") try: # Get transactions transactions = self.parent.transaction_manager.get_transaction_history( ticker=ticker, tx_type=tx_type ) # Update table self.update_table(transactions) # Update ticker filter dropdown self.update_ticker_filter() # Update summary self.lbl_summary.setText(f"Total transactions: {len(transactions)}") logger.info(f"Loaded {len(transactions)} transactions") except Exception as e: logger.error(f"Error loading transactions: {e}", exc_info=True) QMessageBox.critical( self, "Error", f"Failed to load transactions:\n{str(e)}" ) def update_table(self, transactions): """ Update transactions table. Args: transactions: List of transaction dictionaries """ self.table.setSortingEnabled(False) # Disable while updating self.table.setRowCount(len(transactions)) for row, tx in enumerate(transactions): # Store transaction ID in row (hidden) self.table.setRowHeight(row, 25) # Date date_str = format_date(tx['date']) date_item = QTableWidgetItem(date_str) date_item.setData(Qt.ItemDataRole.UserRole, tx['id']) # Store ID self.table.setItem(row, 0, date_item) # Ticker self.table.setItem(row, 1, QTableWidgetItem(tx['ticker'])) # Type (with color coding) tx_type = tx['transaction_type'] type_item = QTableWidgetItem(tx_type) type_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) if tx_type == 'BUY': type_item.setForeground(QBrush(QColor('#28a745'))) # Green elif tx_type == 'SELL': type_item.setForeground(QBrush(QColor('#dc3545'))) # Red elif tx_type == 'DIVIDEND': type_item.setForeground(QBrush(QColor('#ffc107'))) # Yellow self.table.setItem(row, 2, type_item) # Quantity quantity = tx['quantity'] if quantity is not None: qty_item = QTableWidgetItem(f"{quantity:.0f}") qty_item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) else: qty_item = QTableWidgetItem("-") qty_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setItem(row, 3, qty_item) # Price price = tx['price'] if price is not None: price_item = QTableWidgetItem(f"€ {price:.2f}") price_item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) else: price_item = QTableWidgetItem("-") price_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setItem(row, 4, price_item) # Amount amount = tx['amount'] amount_item = QTableWidgetItem(format_currency(amount)) amount_item.setTextAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter) self.table.setItem(row, 5, amount_item) # Notes notes = tx.get('notes', '') or '' self.table.setItem(row, 6, QTableWidgetItem(notes)) self.table.setSortingEnabled(True) # Re-enable sorting self.table.sortItems(0, Qt.SortOrder.DescendingOrder) # Sort by date DESC def update_ticker_filter(self): """Update ticker filter dropdown with unique tickers.""" current_ticker = self.combo_ticker_filter.currentText() # Get unique tickers from database tickers = self.parent.db_manager.get_all_tickers() # Block signals to prevent recursion self.combo_ticker_filter.blockSignals(True) # Update dropdown self.combo_ticker_filter.clear() self.combo_ticker_filter.addItem("All") self.combo_ticker_filter.addItems(sorted(tickers)) # Restore previous selection if still valid index = self.combo_ticker_filter.findText(current_ticker) if index >= 0: self.combo_ticker_filter.setCurrentIndex(index) # Unblock signals self.combo_ticker_filter.blockSignals(False) def apply_filters(self): """Apply type and ticker filters.""" tx_type = self.combo_type_filter.currentText() ticker = self.combo_ticker_filter.currentText() # Convert "All" to None if tx_type == "All": tx_type = None if ticker == "All": ticker = None self.load_data(tx_type=tx_type, ticker=ticker) def on_buy_clicked(self): """Open Buy Transaction dialog.""" dialog = BuyTransactionDialog(self.parent) if dialog.exec(): # Refresh tables self.load_data() self.parent.snapshot_tab.load_data() # Update status bar self.parent.status_bar.showMessage("Buy transaction recorded", 3000) def on_sell_clicked(self): """Open Sell Transaction dialog.""" # Check if there are holdings to sell holdings = [h for h in self.parent.portfolio.holdings if h.quantity > 0 and h.asset_type != 'Cash'] if not holdings: QMessageBox.information( self, "No Holdings", "No holdings available to sell." ) return dialog = SellTransactionDialog(self.parent) if dialog.exec(): # Refresh tables self.load_data() self.parent.snapshot_tab.load_data() # Update status bar self.parent.status_bar.showMessage("Sell transaction recorded", 3000) def on_dividend_clicked(self): """Open Dividend dialog.""" tickers = self.parent.db_manager.get_all_tickers() if not tickers: QMessageBox.information( self, "No Tickers", "No holdings available. Add a holding first." ) return dialog = DividendDialog(self.parent) if dialog.exec(): # Refresh tables self.load_data() # Update status bar self.parent.status_bar.showMessage("Dividend recorded", 3000) def show_context_menu(self, pos): """ Show context menu for table. Args: pos: Click position """ # Get selected row selected_rows = self.table.selectionModel().selectedRows() if not selected_rows: return row = selected_rows[0].row() tx_id = self.table.item(row, 0).data(Qt.ItemDataRole.UserRole) # Create context menu menu = QMenu(self) delete_action = menu.addAction("Delete Transaction") delete_action.triggered.connect(lambda: self.delete_transaction(tx_id)) menu.exec(self.table.viewport().mapToGlobal(pos)) def delete_transaction(self, tx_id): """ Delete a transaction. Args: tx_id: Transaction ID to delete """ # Confirm deletion reply = QMessageBox.question( self, "Confirm Delete", "Delete this transaction?\n\nWARNING: This does NOT reverse the transaction effects on holdings.\nManual adjustment may be needed.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.Yes: try: self.parent.transaction_manager.delete_transaction(tx_id) self.load_data() self.parent.status_bar.showMessage("Transaction deleted", 3000) logger.info(f"Transaction deleted: ID {tx_id}") except Exception as e: logger.error(f"Error deleting transaction: {e}", exc_info=True) QMessageBox.critical( self, "Error", f"Failed to delete transaction:\n{str(e)}" )