Change GUI and start implementing CLI

This commit is contained in:
Andreas Tsouchlos 2024-01-03 02:47:43 +01:00
parent cc8e974589
commit 5e78751921
5 changed files with 274 additions and 119 deletions

View File

@ -1,3 +1,3 @@
# banking-breakdown
Visualize banking statements.
Visualize bank statements.

View File

@ -1,10 +1,54 @@
from banking_breakdown import document_builder
from banking_breakdown import statement_parser
from banking_breakdown import ui
import argparse
def categorize_func(args):
from banking_breakdown.statement_parser import get_stripped_statement
df = None
if args.i is not None:
df = get_stripped_statement(args.i)
ui.show_main_window("res/main_window.ui", df=df)
def report_func(args):
print("Report")
#
# Define CLI
#
def main():
report_data = statement_parser.parse_statement("res/banking_statement_2023.csv")
document_builder.build_document(report_data)
parser = argparse.ArgumentParser(prog='banking_breakdown',
description='Visualize bank statements')
subparsers = parser.add_subparsers()
categorize_parser = subparsers.add_parser("categorize")
categorize_parser.set_defaults(func=categorize_func)
categorize_parser.add_argument('-i', required=False,
help="Bank statement CSV")
categorize_parser.add_argument('-f', required=False,
help="JSON file containing regexes to"
" pre-categorize statement entries")
report_parser = subparsers.add_parser("report")
report_parser.set_defaults(func=report_func)
report_parser.add_argument('-i', required=True,
help="CSV output file from categorization step")
args = parser.parse_args()
if hasattr(args, 'func'):
args.func(args)
else:
parser.print_help()
if __name__ == "__main__":

View File

@ -6,76 +6,92 @@ import re
import numpy as np
def _read_regex_dict(regex_file: str = "res/category_regexes.json"):
with open(regex_file, 'r') as f:
return json.load(f)
# def _read_regex_dict(regex_file: str = "res/category_regexes.json"):
# with open(regex_file, 'r') as f:
# return json.load(f)
#
#
# def _tag_with_category(df: pd.DataFrame) -> pd.DataFrame:
# regex_dict = _read_regex_dict()
#
# return df
#
#
# def _compute_total_balance(df: pd.DataFrame) -> pd.DataFrame:
# stripped_df = pd.DataFrame(
# {'t': df["Valutadatum"], 'value': df["Saldo nach Buchung"]})
#
# stripped_df.index = stripped_df['t']
# gb = stripped_df.groupby(pd.Grouper(freq='M'))
#
# result = gb.tail(1)['value'].reset_index()
#
# return result
#
#
# def _compute_net_income(df: pd.DataFrame) -> pd.DataFrame:
# stripped_df = pd.DataFrame({'t': df["Valutadatum"], 'value': df["Betrag"]})
#
# stripped_df.index = stripped_df['t']
# gb = stripped_df.groupby(pd.Grouper(freq='M'))
#
# result = gb["value"].sum().reset_index()
# return result
#
#
# def _compute_category_overview(df: pd.DataFrame) -> pd.DataFrame:
# categories = ["Social life", "Other", "Food", "Hobbies",
# "Rent \\& Utilities", "Education", "Transportation"]
# values = np.array([10, 12, 53, 12, 90, 23, 32])
# values = values / values.sum() * 100
# values = np.round(values, decimals=1)
# values[-1] += 100 - np.sum(values)
#
# category_overview_df = pd.DataFrame(
# {"category": categories, "value": values})
#
# return category_overview_df
#
#
# def _compute_detailed_balance(df: pd.DataFrame) -> pd.DataFrame:
# return pd.DataFrame({'t': df["Valutadatum"],
# 'value': df["Saldo nach Buchung"]})
#
#
# def parse_statement(filename: str) -> types.ReportData:
# df = pd.read_csv(filename, delimiter=';', decimal=",")
# df["Valutadatum"] = pd.to_datetime(df["Valutadatum"], format='%d.%m.%Y')
#
# category_overview_df = _compute_category_overview(df)
# total_balance_df = _compute_total_balance(df)
# net_income_df = _compute_net_income(df)
# detailed_balance_df = _compute_detailed_balance(df)
#
# return types.ReportData(category_overview_df,
# net_income_df,
# total_balance_df,
# detailed_balance_df)
#
#
# def main():
# report_data = parse_statement("../res/banking_statement_2023.csv")
#
#
# if __name__ == "__main__":
# main()
def _tag_with_category(df: pd.DataFrame) -> pd.DataFrame:
regex_dict = _read_regex_dict()
return df
def _compute_total_balance(df: pd.DataFrame) -> pd.DataFrame:
stripped_df = pd.DataFrame(
{'t': df["Valutadatum"], 'value': df["Saldo nach Buchung"]})
stripped_df.index = stripped_df['t']
gb = stripped_df.groupby(pd.Grouper(freq='M'))
result = gb.tail(1)['value'].reset_index()
return result
def _compute_net_income(df: pd.DataFrame) -> pd.DataFrame:
stripped_df = pd.DataFrame({'t': df["Valutadatum"], 'value': df["Betrag"]})
stripped_df.index = stripped_df['t']
gb = stripped_df.groupby(pd.Grouper(freq='M'))
result = gb["value"].sum().reset_index()
return result
def _compute_category_overview(df: pd.DataFrame) -> pd.DataFrame:
categories = ["Social life", "Other", "Food", "Hobbies",
"Rent \\& Utilities", "Education", "Transportation"]
values = np.array([10, 12, 53, 12, 90, 23, 32])
values = values / values.sum() * 100
values = np.round(values, decimals=1)
values[-1] += 100 - np.sum(values)
category_overview_df = pd.DataFrame(
{"category": categories, "value": values})
return category_overview_df
def _compute_detailed_balance(df: pd.DataFrame) -> pd.DataFrame:
return pd.DataFrame({'t': df["Valutadatum"],
'value': df["Saldo nach Buchung"]})
def parse_statement(filename: str) -> types.ReportData:
def get_stripped_statement(filename: str) -> pd.DataFrame:
df = pd.read_csv(filename, delimiter=';', decimal=",")
df["Valutadatum"] = pd.to_datetime(df["Valutadatum"], format='%d.%m.%Y')
category_overview_df = _compute_category_overview(df)
total_balance_df = _compute_total_balance(df)
net_income_df = _compute_net_income(df)
detailed_balance_df = _compute_detailed_balance(df)
result = pd.DataFrame({'t': df["Valutadatum"],
'other party': df["Name Zahlungsbeteiligter"],
'value': df["Betrag"],
'balance afterwards': df["Saldo nach Buchung"],
'description': df["Buchungstext"],
'purpose': df["Verwendungszweck"]
})
result['category'] = [''] * len(result.index)
return types.ReportData(category_overview_df,
net_income_df,
total_balance_df,
detailed_balance_df)
def main():
report_data = parse_statement("../res/banking_statement_2023.csv")
if __name__ == "__main__":
main()
return result

View File

@ -1,41 +1,118 @@
import sys
import typing
from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, QListView, \
QTableView
from PyQt5 import uic
import pandas as pd
from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, \
QListView, QTableView
from PyQt5 import uic, QtGui, QtCore
import datetime
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, QtCore.Qt.Horizontal, column)
def setHeaderData(self, section, orientation, data,
role=QtCore.Qt.EditRole):
if orientation == QtCore.Qt.Horizontal and role in (
QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
try:
self.horizontalHeaders[section] = data
return True
except:
return False
return super().setHeaderData(section, orientation, data, role)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
try:
return self.horizontalHeaders[section]
except:
pass
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=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
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))
return QtCore.QVariant()
class MainWindow(QMainWindow):
def __init__(self, ui_file):
def __init__(self, ui_file: str, categories: typing.Sequence):
super(MainWindow, self).__init__()
uic.loadUi(ui_file, self)
self.createCategoryButton \
self._createCategoryButton \
= self.findChild(QPushButton, "createCategoryButton")
self.deleteCategoryButton \
self._deleteCategoryButton \
= self.findChild(QPushButton, "deleteCategoryButton")
self.applyCategoryButton \
self._applyCategoryButton \
= self.findChild(QPushButton, "applyCategoryButton")
self.categoryListView = self.findChild(QListView, "categoryListView")
self.statementTableView = self.findChild(QTableView,
"statementTableView")
self._categoryListView = self.findChild(QListView, "categoryListView")
self._statementTableView = self.findChild(QTableView,
"statementTableView")
self.cancelButton = self.findChild(QPushButton, "cancelButton")
self.doneButton = self.findChild(QPushButton, "doneButton")
self._set_categories(categories)
self._define_event_handlers()
self._statementTableView.setSelectionBehavior(QTableView.SelectRows)
def _define_event_handlers(self):
self.cancelButton.clicked.connect(self.close)
def _set_categories(self, categories: typing.Sequence[str]):
model = QtGui.QStandardItemModel()
self._categoryListView.setModel(model)
for category in categories:
item = QtGui.QStandardItem(category)
model.appendRow(item)
def set_statement_data(self, df: pd.DataFrame):
model = PandasModel(df)
self._statementTableView.setModel(model)
def main():
def show_main_window(ui_file, categories: typing.Sequence[str] = None,
df: pd.DataFrame = None):
if categories is None:
categories = []
app = QApplication(sys.argv)
window = MainWindow("../res/main_window.ui")
window = MainWindow(ui_file, categories)
if df is not None:
window.set_statement_data(df)
window.show()
app.exec()
def main():
from banking_breakdown.statement_parser import get_stripped_statement
categories = ["Food", "Rent & Utilities", "Hobbies", "Education",
"Transportation", "Social Life", "Other"]
df = get_stripped_statement("../res/banking_statement_2023.csv")
show_main_window("../res/main_window.ui", categories, df)
if __name__ == "__main__":
main()

View File

@ -21,6 +21,12 @@
<enum>Qt::Horizontal</enum>
</property>
<widget class="QGroupBox" name="groupBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Categories</string>
</property>
@ -85,40 +91,18 @@
</item>
</layout>
</widget>
<widget class="QTableView" name="statementTableView"/>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Bank Statement</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTableView" name="statementTableView"/>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="doneButton">
<property name="text">
<string>Done</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
@ -130,8 +114,42 @@
<height>23</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
<property name="title">
<string>File</string>
</property>
<addaction name="actionOpen"/>
<addaction name="separator"/>
<addaction name="actionSave"/>
<addaction name="actionSave_As"/>
</widget>
<addaction name="menuFile"/>
</widget>
<widget class="QStatusBar" name="statusbar"/>
<action name="actionSave">
<property name="text">
<string>Save</string>
</property>
<property name="shortcut">
<string>Ctrl+S</string>
</property>
</action>
<action name="actionSave_As">
<property name="text">
<string>Save As...</string>
</property>
<property name="shortcut">
<string>Ctrl+Shift+S</string>
</property>
</action>
<action name="actionOpen">
<property name="text">
<string>Open...</string>
</property>
<property name="shortcut">
<string>Ctrl+O</string>
</property>
</action>
</widget>
<resources/>
<connections/>