banking-breakdown/banking_breakdown/ui.py

234 lines
7.7 KiB
Python

import sys
import typing
from functools import partial
import pandas as pd
from PyQt6 import uic, QtGui, QtCore
from PyQt6.QtCore import Qt, QSortFilterProxyModel
from PyQt6.QtGui import QPixmap, QAction
from PyQt6.QtWidgets import QMainWindow, QPushButton, QHBoxLayout, QLabel, \
QVBoxLayout, QMenu, QApplication, QTableView, QListView, QInputDialog, \
QMessageBox
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, df: pd.DataFrame, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = df
self._horizontalHeaders = [''] * len(df.columns)
for i, column in enumerate(df.columns):
self.setHeaderData(i, Qt.Orientation.Horizontal, column)
def setHeaderData(self, section, orientation, data,
role=Qt.ItemDataRole.EditRole):
if ((orientation == Qt.Orientation.Horizontal)
and ((role == Qt.ItemDataRole.DisplayRole)
or (role == Qt.ItemDataRole.EditRole))):
self._horizontalHeaders[section] = data
return True
else:
return super().setHeaderData(section, orientation, data, role)
def headerData(self, section, orientation,
role=Qt.ItemDataRole.DisplayRole):
if (orientation == Qt.Orientation.Horizontal
and role == Qt.ItemDataRole.DisplayRole):
return self._horizontalHeaders[section]
else:
return super().headerData(section, orientation, role)
def rowCount(self, parent=None):
return len(self._data.values)
def columnCount(self, parent=None):
return self._data.columns.size
def data(self, index, role=Qt.ItemDataRole.DisplayRole):
if not index.isValid():
return QtCore.QVariant()
if role != Qt.ItemDataRole.DisplayRole:
return QtCore.QVariant()
item = self._data.iloc[index.row()].iloc[index.column()]
if type(item) is pd.Timestamp:
return QtCore.QVariant(item.strftime('%Y-%m-%d'))
else:
return QtCore.QVariant(str(item))
def get_dataframe(self):
return self._data
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
uic.loadUi("res/main_window.ui", self)
self.setContentsMargins(9, 9, 9, 9)
self._warning_layout \
= self.findChild(QVBoxLayout, "warningLayout")
self._create_button \
= self.findChild(QPushButton, "createCategoryButton")
self._delete_button \
= self.findChild(QPushButton, "deleteCategoryButton")
self._apply_button \
= self.findChild(QPushButton, "applyCategoryButton")
self._list_view \
= self.findChild(QListView, "categoryListView")
self._table_view \
= self.findChild(QTableView, "transactionTableView")
self._category_model = QtGui.QStandardItemModel()
self._list_view.setModel(self._category_model)
self._create_button.clicked.connect(
self._handle_create_click)
header = self._table_view.horizontalHeader()
header.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
header.customContextMenuRequested.connect(self._header_right_clicked)
def add_warning_text(self, text: str):
layout = QHBoxLayout()
warningIcon = QLabel()
pixmap = QPixmap("res/warning.png")
warningIcon.setPixmap(pixmap)
label = QLabel(text)
label.setWordWrap(True)
layout.addWidget(warningIcon)
layout.addWidget(label)
layout.setStretch(0, 0)
layout.setStretch(1, 1)
self._warning_layout.addLayout(layout)
def set_statement_data(self, df: pd.DataFrame):
model = PandasModel(df)
proxyModel = QSortFilterProxyModel(self)
proxyModel.setSourceModel(model)
self._table_view.setModel(proxyModel)
if len(df.columns) < 7:
self._table_view.resizeColumnsToContents()
else:
for i, col in enumerate(df.columns):
max_char = max(max([len(str(x)) for x in df[col].values]),
len(col))
self._table_view.setColumnWidth(i, max_char * 10)
def get_statement_data(self) -> pd.DataFrame:
return self._table_view.model().sourceModel().get_dataframe()
def add_categories(self, categories: typing.Sequence[str]):
for category in categories:
item = QtGui.QStandardItem(category)
self._category_model.appendRow(item)
def _header_right_clicked(self, pos):
context = QMenu(self)
rename_action = QAction("Rename", self)
delete_action = QAction("Delete", self)
column = self._table_view.horizontalHeader().logicalIndexAt(pos)
rename_action.triggered.connect(
partial(self._header_rename_handler, column))
delete_action.triggered.connect(
partial(self._header_delete_handler, column))
context.addAction(rename_action)
context.addAction(delete_action)
context.exec(self.sender().mapToGlobal(pos))
def _header_rename_handler(self, column):
model = self._table_view.horizontalHeader().model()
column_text = model.headerData(column, Qt.Orientation.Horizontal)
new_name, _ = QInputDialog.getText(self, "Rename column", "New name:",
text=column_text)
if (new_name != column_text) and (new_name != ''):
df = self.get_statement_data()
df = df.rename(columns={column_text: new_name})
self.set_statement_data(df)
def _header_delete_handler(self, column):
model = self._table_view.horizontalHeader().model()
column_text = model.headerData(column, Qt.Orientation.Horizontal)
button = QMessageBox.question(self, "Delete column",
f"Are you sure you want to delete"
f" column '{column_text}'?")
if button == QMessageBox.StandardButton.Yes:
df = self.get_statement_data()
df = df.iloc[:, [j for j, c
in enumerate(df.columns) if j != column]]
self.set_statement_data(df)
def _handle_create_click(self):
self.add_categories(['asdf'])
def _handle_delete_click(self):
pass
def _handle_apply_click(self):
pass
def show_main_window(categories: typing.Sequence[str] = None,
df: pd.DataFrame = None):
app = QApplication(sys.argv)
window = MainWindow()
if categories is not None:
window.add_categories(categories)
if df is not None:
window.set_statement_data(df)
window.add_warning_text("The column 't' does not exist. Please rename the"
" column containing the dates of the transactions"
" to 't'.")
window.add_warning_text("The column 'balance' does not exist. Please"
" rename the column containing the balance after"
" each transaction to 'balance'")
window.add_warning_text("The column 'value' does not exist. Please rename"
" the column containing the values of the"
" transactions to 'value'.")
window.show()
app.exec()
def main():
from banking_breakdown.statement_parser import get_stripped_statement
import os
os.chdir("../")
categories = ["Food", "Rent & Utilities", "Hobbies", "Education",
"Transportation", "Social Life", "Other"]
df = pd.read_csv("res/bank_statement_2023.csv", delimiter=';')
show_main_window(categories, df)
if __name__ == "__main__":
main()