diff --git a/README.md b/README.md
index 7f9f502..f190aa1 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
# banking-breakdown
-Visualize banking statements.
\ No newline at end of file
+Visualize bank statements.
\ No newline at end of file
diff --git a/banking_breakdown/__main__.py b/banking_breakdown/__main__.py
index 8f3af0c..8aee9c0 100644
--- a/banking_breakdown/__main__.py
+++ b/banking_breakdown/__main__.py
@@ -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__":
diff --git a/banking_breakdown/statement_parser.py b/banking_breakdown/statement_parser.py
index b64da83..fce14df 100644
--- a/banking_breakdown/statement_parser.py
+++ b/banking_breakdown/statement_parser.py
@@ -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
diff --git a/banking_breakdown/ui.py b/banking_breakdown/ui.py
index 5ea4ed6..cc167c2 100644
--- a/banking_breakdown/ui.py
+++ b/banking_breakdown/ui.py
@@ -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()
diff --git a/res/main_window.ui b/res/main_window.ui
index 434b1ac..cd803a2 100644
--- a/res/main_window.ui
+++ b/res/main_window.ui
@@ -21,6 +21,12 @@
Qt::Horizontal
+
+
+ 0
+ 0
+
+
Categories
@@ -85,40 +91,18 @@
-
+
+
+ Bank Statement
+
+
+ -
+
+
+
+
- -
-
-
-
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
- -
-
-
- Cancel
-
-
-
- -
-
-
- Done
-
-
-
-
-
+
+
+ Save
+
+
+ Ctrl+S
+
+
+
+
+ Save As...
+
+
+ Ctrl+Shift+S
+
+
+
+
+ Open...
+
+
+ Ctrl+O
+
+