Change GUI and start implementing CLI
This commit is contained in:
parent
cc8e974589
commit
5e78751921
@ -1,3 +1,3 @@
|
||||
# banking-breakdown
|
||||
|
||||
Visualize banking statements.
|
||||
Visualize bank statements.
|
||||
@ -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__":
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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/>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user