Change GUI and start implementing CLI
This commit is contained in:
parent
cc8e974589
commit
5e78751921
@ -1,3 +1,3 @@
|
|||||||
# banking-breakdown
|
# banking-breakdown
|
||||||
|
|
||||||
Visualize banking statements.
|
Visualize bank statements.
|
||||||
@ -1,10 +1,54 @@
|
|||||||
from banking_breakdown import document_builder
|
from banking_breakdown import document_builder
|
||||||
from banking_breakdown import statement_parser
|
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():
|
def main():
|
||||||
report_data = statement_parser.parse_statement("res/banking_statement_2023.csv")
|
parser = argparse.ArgumentParser(prog='banking_breakdown',
|
||||||
document_builder.build_document(report_data)
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -6,76 +6,92 @@ import re
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
def _read_regex_dict(regex_file: str = "res/category_regexes.json"):
|
# def _read_regex_dict(regex_file: str = "res/category_regexes.json"):
|
||||||
with open(regex_file, 'r') as f:
|
# with open(regex_file, 'r') as f:
|
||||||
return json.load(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:
|
def get_stripped_statement(filename: str) -> 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 = pd.read_csv(filename, delimiter=';', decimal=",")
|
||||||
df["Valutadatum"] = pd.to_datetime(df["Valutadatum"], format='%d.%m.%Y')
|
df["Valutadatum"] = pd.to_datetime(df["Valutadatum"], format='%d.%m.%Y')
|
||||||
|
|
||||||
category_overview_df = _compute_category_overview(df)
|
result = pd.DataFrame({'t': df["Valutadatum"],
|
||||||
total_balance_df = _compute_total_balance(df)
|
'other party': df["Name Zahlungsbeteiligter"],
|
||||||
net_income_df = _compute_net_income(df)
|
'value': df["Betrag"],
|
||||||
detailed_balance_df = _compute_detailed_balance(df)
|
'balance afterwards': df["Saldo nach Buchung"],
|
||||||
|
'description': df["Buchungstext"],
|
||||||
|
'purpose': df["Verwendungszweck"]
|
||||||
|
})
|
||||||
|
result['category'] = [''] * len(result.index)
|
||||||
|
|
||||||
return types.ReportData(category_overview_df,
|
return result
|
||||||
net_income_df,
|
|
||||||
total_balance_df,
|
|
||||||
detailed_balance_df)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
report_data = parse_statement("../res/banking_statement_2023.csv")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|||||||
@ -1,41 +1,118 @@
|
|||||||
import sys
|
import sys
|
||||||
|
import typing
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, QListView, \
|
import pandas as pd
|
||||||
QTableView
|
from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, \
|
||||||
from PyQt5 import uic
|
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):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self, ui_file):
|
def __init__(self, ui_file: str, categories: typing.Sequence):
|
||||||
super(MainWindow, self).__init__()
|
super(MainWindow, self).__init__()
|
||||||
uic.loadUi(ui_file, self)
|
uic.loadUi(ui_file, self)
|
||||||
|
|
||||||
self.createCategoryButton \
|
self._createCategoryButton \
|
||||||
= self.findChild(QPushButton, "createCategoryButton")
|
= self.findChild(QPushButton, "createCategoryButton")
|
||||||
self.deleteCategoryButton \
|
self._deleteCategoryButton \
|
||||||
= self.findChild(QPushButton, "deleteCategoryButton")
|
= self.findChild(QPushButton, "deleteCategoryButton")
|
||||||
self.applyCategoryButton \
|
self._applyCategoryButton \
|
||||||
= self.findChild(QPushButton, "applyCategoryButton")
|
= self.findChild(QPushButton, "applyCategoryButton")
|
||||||
|
|
||||||
self.categoryListView = self.findChild(QListView, "categoryListView")
|
self._categoryListView = self.findChild(QListView, "categoryListView")
|
||||||
self.statementTableView = self.findChild(QTableView,
|
self._statementTableView = self.findChild(QTableView,
|
||||||
"statementTableView")
|
"statementTableView")
|
||||||
|
|
||||||
self.cancelButton = self.findChild(QPushButton, "cancelButton")
|
self._set_categories(categories)
|
||||||
self.doneButton = self.findChild(QPushButton, "doneButton")
|
|
||||||
|
|
||||||
self._define_event_handlers()
|
self._statementTableView.setSelectionBehavior(QTableView.SelectRows)
|
||||||
|
|
||||||
def _define_event_handlers(self):
|
def _set_categories(self, categories: typing.Sequence[str]):
|
||||||
self.cancelButton.clicked.connect(self.close)
|
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)
|
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()
|
window.show()
|
||||||
app.exec()
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@ -21,6 +21,12 @@
|
|||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Categories</string>
|
<string>Categories</string>
|
||||||
</property>
|
</property>
|
||||||
@ -85,39 +91,17 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<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"/>
|
<widget class="QTableView" name="statementTableView"/>
|
||||||
</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>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
@ -130,8 +114,42 @@
|
|||||||
<height>23</height>
|
<height>23</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</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>
|
||||||
<widget class="QStatusBar" name="statusbar"/>
|
<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>
|
</widget>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections/>
|
<connections/>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user