From df436fa2b23956b8619c42d078b30f0d9cec831a Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Mon, 14 Apr 2025 16:25:21 +0100 Subject: [PATCH 01/15] begin implementation of sqlite database --- gui/dialogs/platform_rename.ui | 55 +++++++++++++++ gui/main_gui.ui | 6 +- gui/platform_list.ui | 124 +++++++++++++++++++++++++++++++++ src/db_handler.py | 106 ++++++++++++++++++++++++++++ src/main_window.py | 30 ++++++-- src/platform_edit.py | 2 +- src/platform_list.py | 44 ++++++++++++ src/resource_finder.py | 2 +- 8 files changed, 360 insertions(+), 9 deletions(-) create mode 100644 gui/dialogs/platform_rename.ui create mode 100644 gui/platform_list.ui create mode 100644 src/db_handler.py create mode 100644 src/platform_list.py diff --git a/gui/dialogs/platform_rename.ui b/gui/dialogs/platform_rename.ui new file mode 100644 index 0000000..b18d19a --- /dev/null +++ b/gui/dialogs/platform_rename.ui @@ -0,0 +1,55 @@ + + + PlatformRename + + + + 0 + 0 + 300 + 90 + + + + Rename Platform + + + + + 221 + 62 + 75 + 24 + + + + OK + + + + + + 60 + 10 + 191 + 20 + + + + Enter a new name for the platform + + + + + + 8 + 34 + 287 + 22 + + + + + + + diff --git a/gui/main_gui.ui b/gui/main_gui.ui index 8b5f2de..f618c7f 100644 --- a/gui/main_gui.ui +++ b/gui/main_gui.ui @@ -220,13 +220,13 @@ File - + - + - Edit Platforms + Platform List diff --git a/gui/platform_list.ui b/gui/platform_list.ui new file mode 100644 index 0000000..fe6679a --- /dev/null +++ b/gui/platform_list.ui @@ -0,0 +1,124 @@ + + + PlatformList + + + + 0 + 0 + 264 + 473 + + + + Platform List + + + + + 10 + 34 + 243 + 371 + + + + + 11 + + + + + + + 11 + 413 + 121 + 24 + + + + + 10 + + + + Add platform + + + + + + 135 + 413 + 121 + 24 + + + + + 10 + + + + Remove platform + + + + + + 136 + 442 + 121 + 24 + + + + + 10 + + + + Save + + + + + + 11 + 442 + 121 + 24 + + + + + 10 + + + + Edit platform + + + + + + 12 + 8 + 231 + 20 + + + + + 11 + + + + Platform enabled? + + + + + + diff --git a/src/db_handler.py b/src/db_handler.py new file mode 100644 index 0000000..e8cd137 --- /dev/null +++ b/src/db_handler.py @@ -0,0 +1,106 @@ +import os +import sqlite3 + + +class DBHandler: + def __init__(self): + def create_tables(): + self.cur.execute(""" + CREATE TABLE "tblPlatforms" ( + "PlatformID" INTEGER NOT NULL UNIQUE, + "PlatformName" TEXT NOT NULL, + PRIMARY KEY("PlatformID") + ) + """) + + self.cur.execute(""" + CREATE TABLE "tblFlatPlatFees" ( + "PlatformID" INTEGER NOT NULL, + "SharePlatFee" REAL NOT NULL, + "SharePlatMaxFee" REAL, + PRIMARY KEY("PlatformID"), + FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID") + ) + """) + + self.cur.execute(""" + CREATE TABLE "tblFlatDealFees" ( + "PlatformID" INTEGER NOT NULL, + "FundDealFee" REAL, + "ShareDealFee" REAL NOT NULL, + "ShareDealReduceTrades" REAL, + "ShareDealReduceAmount" REAL, + PRIMARY KEY("PlatformID"), + FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID") + ) + """) + + self.cur.execute(""" + CREATE TABLE "tblFundPlatFee" ( + "PlatformID" INTEGER NOT NULL, + "Band" REAL NOT NULL, + "Fee" REAL NOT NULL, + PRIMARY KEY("PlatformID","Band","Fee"), + FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID") + ) + """) + + self.cur.execute(""" + CREATE TABLE "tblUserDetails" ( + "PensionValue" REAL, + "SliderValue" INTEGER, + "ShareTrades" INTEGER, + "FundTrades" INTEGER + ) + """) + + if not os.path.exists("SIPPCompare.db"): + db_exists = False + else: + db_exists = True + + self.conn = sqlite3.connect("SIPPCompare.db") + self.cur = self.conn.cursor() + if not db_exists: + create_tables() + + def retrieve_plat_list(self) -> list: + res = self.cur.execute("SELECT PlatformName FROM tblPlatforms") + res_list = res.fetchall() + plat_name_list = [] + for platform in res_list: + plat_name_list.append(platform[0]) + + return plat_name_list + + def write_user_details(self, pension_val: float, slider_val: int, share_trades: int, fund_trades: int): + user_details_data = (pension_val, slider_val, share_trades, fund_trades) + + res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone() + if res[0] == 0: + self.cur.execute("INSERT INTO tblUserDetails VALUES (?, ?, ?, ?)", user_details_data) + else: + self.cur.execute(""" + UPDATE tblUserDetails SET + PensionValue = ?, + SliderValue = ?, + ShareTrades = ?, + FundTrades = ? + """, user_details_data) + self.conn.commit() + + def retrieve_user_details(self) -> dict: + res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone() + if res[0] == 0: + return {"NO_RECORD": None} + + res = self.cur.execute("SELECT * FROM tblUserDetails") + res_tuple = res.fetchone() + user_details_dict = { + "pension_val": res_tuple[0], + "slider_val": res_tuple[1], + "share_trades": res_tuple[2], + "fund_trades": res_tuple[3] + } + + return user_details_dict diff --git a/src/main_window.py b/src/main_window.py index d56484a..dc12513 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -3,7 +3,9 @@ from PyQt6.QtWidgets import QMainWindow, QWidget from PyQt6 import uic import output_window +import platform_list import resource_finder +import db_handler class SIPPCompare(QMainWindow): @@ -33,13 +35,16 @@ class SIPPCompare(QMainWindow): self.share_deal_fees = 0.0 # Create window objects + self.db = db_handler.DBHandler() self.platform_win = plat_edit_win + self.platform_list_win = platform_list.PlatformList(self.db) self.output_win = output_window.OutputWindow() # Handle events self.calc_but.clicked.connect(self.calculate_fees) # Menu bar entry (File -> Edit Platforms) - self.actionEdit_Platforms.triggered.connect(self.show_platform_edit) + #self.actionEdit_Platforms.triggered.connect(self.show_platform_edit) + self.actionList_Platforms.triggered.connect(self.show_platform_list) # Update percentage mix label when slider moved self.mix_slider.valueChanged.connect(self.update_slider_lab) self.value_input.valueChanged.connect(self.check_valid) @@ -50,6 +55,15 @@ class SIPPCompare(QMainWindow): self.share_trades_combo.setValidator(QIntValidator(0, 999)) self.fund_trades_combo.setValidator(QIntValidator(0, 99)) + # Restore last session + prev_session_data = self.db.retrieve_user_details() + if "NO_RECORD" not in prev_session_data: + self.value_input.setValue(prev_session_data["pension_val"]) + self.mix_slider.setValue(prev_session_data["slider_val"]) + self.share_trades_combo.setCurrentText(str(prev_session_data["share_trades"])) + self.fund_trades_combo.setCurrentText(str(prev_session_data["fund_trades"])) + self.calc_but.setFocus() + # Display slider position as mix between two nums (funds/shares) def update_slider_lab(self): slider_val = self.mix_slider.value() @@ -100,13 +114,18 @@ class SIPPCompare(QMainWindow): # Calculate fees def calculate_fees(self): self.init_variables() + # Set to zero each time to avoid persistence self.fund_plat_fees = 0 + + # Get user input value_num = float(self.value_input.value()) - # Funds/shares mix slider_val: int = self.mix_slider.value() - funds_value = (slider_val / 100) * value_num fund_trades_num = int(self.fund_trades_combo.currentText()) + share_trades_num = int(self.share_trades_combo.currentText()) + + # Funds/shares mix + funds_value = (slider_val / 100) * value_num if self.fund_deal_fee is not None: self.fund_deal_fees = fund_trades_num * self.fund_deal_fee @@ -130,13 +149,13 @@ class SIPPCompare(QMainWindow): else: self.share_plat_fees = self.share_plat_fee * shares_value - share_trades_num = int(self.share_trades_combo.currentText()) if self.share_deal_reduce_trades is not None: if (share_trades_num / 12) >= self.share_deal_reduce_trades: self.share_deal_fees = self.share_deal_reduce_amount * share_trades_num else: self.share_deal_fees = self.share_deal_fee * share_trades_num + self.db.write_user_details(value_num, slider_val, share_trades_num, fund_trades_num) self.show_output_win() # Show the output window - this func is called from calculate_fee() @@ -151,3 +170,6 @@ class SIPPCompare(QMainWindow): # Show the platform editor window (currently run-time only) def show_platform_edit(self): self.platform_win.show() + + def show_platform_list(self): + self.platform_list_win.show() diff --git a/src/platform_edit.py b/src/platform_edit.py index 663d8f8..492a9fb 100644 --- a/src/platform_edit.py +++ b/src/platform_edit.py @@ -93,7 +93,7 @@ class PlatformEdit(QWidget): QRegularExpressionValidator(QRegularExpression("\\w*")) ) - def create_plat_fee_struct(self): + def create_plat_fee_struct(self) -> list: plat_fee_struct = [[0], [0]] plat_fee_struct[0].append(self.first_tier_box.value()) plat_fee_struct[1].append(self.first_tier_fee_box.value()) diff --git a/src/platform_list.py b/src/platform_list.py new file mode 100644 index 0000000..91cccc9 --- /dev/null +++ b/src/platform_list.py @@ -0,0 +1,44 @@ +from PyQt6.QtWidgets import QWidget, QListWidgetItem +from PyQt6.QtGui import QIcon, QRegularExpressionValidator +from PyQt6.QtCore import QRegularExpression +from PyQt6 import uic + +import resource_finder + + +class PlatformRename(QWidget): + def __init__(self): + super().__init__() + # Import Qt Designer UI XML file + uic.loadUi(resource_finder.get_res_path("gui/dialogs/platform_rename.ui"), self) + self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + + # Set validators + # Regex accepts any characters that match [a-Z], [0-9] or _ + self.rename_plat_box.setValidator( + QRegularExpressionValidator(QRegularExpression("\\w*")) + ) + + +class PlatformList(QWidget): + def __init__(self, db): + super().__init__() + # Import Qt Designer UI XML file + uic.loadUi(resource_finder.get_res_path("gui/platform_list.ui"), self) + self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + + self.plat_list_dialog = PlatformRename() + self.db = db + self.plat_name_list = self.db.retrieve_plat_list() + print(self.plat_name_list) + + for platform in self.plat_name_list: + item = QListWidgetItem() + item.setText(platform) + self.platListWidget.addItem(item) + + # Handle events + self.add_plat_but.clicked.connect(self.add_platform) + + def add_platform(self): + self.plat_list_dialog.show() diff --git a/src/resource_finder.py b/src/resource_finder.py index d0e006a..f2b33e2 100644 --- a/src/resource_finder.py +++ b/src/resource_finder.py @@ -10,4 +10,4 @@ def get_res_path(relative_path): except AttributeError: base_path = os.path.abspath(".") - return os.path.join(base_path, relative_path) \ No newline at end of file + return os.path.join(base_path, relative_path) From 0fa6db3bc0863b45fee2f1953d2b04343e5b02f0 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Tue, 22 Apr 2025 13:37:43 +0100 Subject: [PATCH 02/15] read platform details from database (WIP) --- SIPPCompare.db | Bin 0 -> 53248 bytes src/db_handler.py | 90 ++++++++++++++++++----- src/main.py | 7 +- src/main_window.py | 11 +-- src/platform_edit.py | 169 +++++++++++++++++++++++++++++-------------- src/platform_list.py | 28 +++++++ 6 files changed, 221 insertions(+), 84 deletions(-) create mode 100644 SIPPCompare.db diff --git a/SIPPCompare.db b/SIPPCompare.db new file mode 100644 index 0000000000000000000000000000000000000000..ef44f752cfd449b182e401ab5439891013c26e56 GIT binary patch literal 53248 zcmeI*PiP}m90%|>nItolZu_j%ja6HRV5PCevhIRNS<-YnDY0qVrm5Idm~LhqWz0lk(oNbFxFXW75-q7=n-hpPx&4qnSp4(gUoaU5FPG$@} z)|MXAha*~SzHHXSZxQLKn3m4wjcFsNXXo;Ib}5t5m$K=FC1aGbExu=NSR!xAvSJ!F zDdXnyCC{2y-5iuKpG(gsbIbaSu{`X3t2m{l=5j`QI@_M6=Zut*GqM+qMLkwqDLYN9 z#{6|e;*C(h5{<{DO*3KDXV6d%2ofK;@9ve+7zN}2Znj8QZ21j-Z9JdeHqH7 zqP5XV-K*(3>FM)@9RLD!7lBS zbOxtX)?6ipyzy>cG(BB4Dxyyp_dPOS%bf~Hn_AzHd+kp#>9k%bh&oB z5<7Qj(acG+LM?3T!EUeh_Up$rdep9EG$u+NQeWnM?3L)qh_sn-6-c0^K%Ke42b4w^ z%>g3M?EZPmDNm6-<6l5}QafWE|Iu&ITuK^SyZwd^aLO{v^s$KHFOJvQb>~rgHr$i? zL>i*YwW_r{k~^|}vEGhk&z~YWXBF!gEoX>}Zs;p*Cns0e>Xn*X`Qy6bA@#iR7sAoG zxWr18qIH$NCg{YnVAgAE&HVyNc_!7u8UKFQo2SFknV|y;J7c}tEb8us=L2E!;Vn0N za_e2I*5$fYxhTFCMI)SD**EIcClAK*VS{tG=IKmY;|fB*y_009U< z00Izz00ba#0t8M=bb_xiHZf>7)_H#3KJix)t$X}K5%1ss2l!8nKcWW&2tWV=5P$## zAOHafKmY;|fB*zenm{-G$3I0%tgp=8{o~e7WdHU5#NYp`{6n$+->>{0|BipnKj$|n z0s#ULfB*y_009U<00Izz00bZafx{N)4biH8l4s#R)sFG3{HNG4o~8f7;SeqTCxv=L zw6dS%S^O_($9Pu%lkFJK@_&I)h?f7;#QDF(cWC{;!#*U`2LT8`00Izz00bZa0SG_< z0uX?}aS#ye|6%?A list: - res = self.cur.execute("SELECT PlatformName FROM tblPlatforms") - res_list = res.fetchall() + res = self.cur.execute("SELECT PlatformName FROM tblPlatforms").fetchall() plat_name_list = [] - for platform in res_list: + for platform in res: plat_name_list.append(platform[0]) return plat_name_list + def retrieve_platforms(self) -> list[Platform]: + platforms_res = self.cur.execute(""" + SELECT + -- tblPlatforms + tblPlatforms.PlatformID, PlatformName, IsEnabled, + -- tblFlatPlatFees + SharePlatFee, SharePlatMaxFee, + -- tblFlatDealFees + FundDealFee, + ShareDealFee, + ShareDealReduceTrades, + ShareDealReduceAmount + FROM tblPlatforms + INNER JOIN tblFlatPlatFees ON + tblPlatforms.PlatformID = tblFlatPlatFees.PlatformID + INNER JOIN tblFlatDealFees ON + tblPlatforms.PlatformID = tblFlatDealFees.PlatformID + """).fetchall() + + platforms = [] + for platform in platforms_res: + # plat_id, plat_name, enabled, share_plat_fee, share_plat_max_fee, fund_deal_fee, + # share_deal_fee, share_deal_reduce_trades, share_deal_reduce_amount + this_platform = [platform[0], platform[1], platform[2], platform[3], platform[4], + platform[5], platform[6], platform[7], platform[8]] + platforms.append(this_platform) + + for platform in platforms: + platform.insert(1, [[], []]) + + fund_plat_fee_res = self.cur.execute("SELECT * FROM tblFundPlatFee").fetchall() + for i in range(len(fund_plat_fee_res)): + plat_id = fund_plat_fee_res[i][0] + platforms[plat_id][1][0].append(fund_plat_fee_res[i][1]) + platforms[plat_id][1][1].append(fund_plat_fee_res[i][2]) + + platform_obj_list: list[Platform] = [] + for platform in platforms: + platform_obj_list.append(Platform( + platform[0], platform[1], platform[2], platform[3], + platform[6], platform[4], platform[5], platform[7], + platform[8], platform[9] + )) + + return platform_obj_list + + def write_user_details(self, pension_val: float, slider_val: int, share_trades: int, fund_trades: int): - user_details_data = (pension_val, slider_val, share_trades, fund_trades) + # Hardcode UserID as 0 + user_details_data = (0, pension_val, slider_val, share_trades, fund_trades) res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone() if res[0] == 0: - self.cur.execute("INSERT INTO tblUserDetails VALUES (?, ?, ?, ?)", user_details_data) + self.cur.execute("INSERT INTO tblUserDetails VALUES (?, ?, ?, ?, ?)", user_details_data) else: self.cur.execute(""" UPDATE tblUserDetails SET + UserID = ?, PensionValue = ?, SliderValue = ?, ShareTrades = ?, @@ -95,12 +148,13 @@ class DBHandler: return {"NO_RECORD": None} res = self.cur.execute("SELECT * FROM tblUserDetails") - res_tuple = res.fetchone() - user_details_dict = { - "pension_val": res_tuple[0], - "slider_val": res_tuple[1], - "share_trades": res_tuple[2], - "fund_trades": res_tuple[3] + res_tuple: tuple = res.fetchone() + user_details_dict: dict[str, float | int] = { + "user_id": res_tuple[0], + "pension_val": res_tuple[1], + "slider_val": res_tuple[2], + "share_trades": res_tuple[3], + "fund_trades": res_tuple[4] } return user_details_dict diff --git a/src/main.py b/src/main.py index 14c9b4d..4ee5f19 100644 --- a/src/main.py +++ b/src/main.py @@ -3,18 +3,21 @@ from PyQt6.QtWidgets import QApplication import sys import platform_edit +import main_window app = QApplication(sys.argv) # Show platform edit window first, before main win # When debugging, can be useful to autofill values to save time -if len(sys.argv) > 1: +"""if len(sys.argv) > 1: if sys.argv[1] == "--DEBUG_AUTOFILL": window = platform_edit.PlatformEdit(True) else: window = platform_edit.PlatformEdit(False) else: - window = platform_edit.PlatformEdit(False) + window = platform_edit.PlatformEdit(False)""" +#plat_edit_win = platform_edit.PlatformEdit() +window = main_window.SIPPCompare() window.show() app.exec() diff --git a/src/main_window.py b/src/main_window.py index dc12513..f500469 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -9,8 +9,7 @@ import db_handler class SIPPCompare(QMainWindow): - # Receive instance of PlatformEdit() as parameter - def __init__(self, plat_edit_win: QWidget): + def __init__(self): super().__init__() # Import Qt Designer UI XML file uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self) @@ -36,14 +35,12 @@ class SIPPCompare(QMainWindow): # Create window objects self.db = db_handler.DBHandler() - self.platform_win = plat_edit_win self.platform_list_win = platform_list.PlatformList(self.db) self.output_win = output_window.OutputWindow() # Handle events self.calc_but.clicked.connect(self.calculate_fees) # Menu bar entry (File -> Edit Platforms) - #self.actionEdit_Platforms.triggered.connect(self.show_platform_edit) self.actionList_Platforms.triggered.connect(self.show_platform_list) # Update percentage mix label when slider moved self.mix_slider.valueChanged.connect(self.update_slider_lab) @@ -64,6 +61,8 @@ class SIPPCompare(QMainWindow): self.fund_trades_combo.setCurrentText(str(prev_session_data["fund_trades"])) self.calc_but.setFocus() + + # Display slider position as mix between two nums (funds/shares) def update_slider_lab(self): slider_val = self.mix_slider.value() @@ -167,9 +166,5 @@ class SIPPCompare(QMainWindow): ) self.output_win.show() - # Show the platform editor window (currently run-time only) - def show_platform_edit(self): - self.platform_win.show() - def show_platform_list(self): self.platform_list_win.show() diff --git a/src/platform_edit.py b/src/platform_edit.py index 492a9fb..07b8817 100644 --- a/src/platform_edit.py +++ b/src/platform_edit.py @@ -4,36 +4,30 @@ from PyQt6.QtWidgets import QWidget, QLabel from PyQt6 import uic from widgets.fastedit_spinbox import FastEditQDoubleSpinBox +from data_struct import Platform import main_window import resource_finder class PlatformEdit(QWidget): - def __init__(self, autofill: bool): + def __init__(self, plat: Platform): super().__init__() # Import Qt Designer UI XML file uic.loadUi(resource_finder.get_res_path("gui/platform_edit.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) # Initialise class variables - # Create main window object, passing this instance as param - self.main_win = main_window.SIPPCompare(self) - - self.fund_plat_fee = [] - self.plat_name = "" - self.fund_deal_fee = 0.0 - self.share_plat_fee = 0.0 - self.share_plat_max_fee = 0.0 - self.share_deal_fee = 0.0 - self.share_deal_reduce_trades = 0.0 - self.share_deal_reduce_amount = 0.0 + self.plat = plat + self.fund_plat_fee = self.plat.fund_plat_fee self.widgets_list_list = [] - self.fund_fee_rows = 1 + self.fund_fee_rows = len(self.plat.fund_plat_fee[0]) + """ # Debugging feature: set with "--DEBUG_AUTOFILL" cmd argument self.autofill = autofill if autofill: self.save_but.setEnabled(True) + """ self.required_fields = [ self.share_plat_fee_box, @@ -64,6 +58,54 @@ class PlatformEdit(QWidget): False ] + if self.plat.plat_name is None: + self.check_boxes_ticked[0] = False + self.plat_name_check.setChecked(False) + else: + self.check_boxes_ticked[0] = True + self.plat_name_check.setChecked(True) + self.plat_name_box.setText(self.plat.plat_name) + + if self.plat.fund_deal_fee is None: + self.check_boxes_ticked[1] = False + self.plat_fund_deal_fee_check.setChecked(False) + else: + self.check_boxes_ticked[1] = True + self.fund_deal_fee_check.setChecked(True) + self.fund_deal_fee_box.setValue(self.plat.fund_deal_fee) + + self.share_plat_fee_box.setValue(self.plat.share_plat_fee * 100) + + if self.plat.share_plat_max_fee is None: + self.check_boxes_ticked[2] = False + self.share_plat_max_fee_check.setChecked(False) + else: + self.check_boxes_ticked[2] = True + self.share_plat_max_fee_check.setChecked(True) + self.share_plat_max_fee_box.setValue(self.plat.share_plat_max_fee) + + self.share_deal_fee_box.setValue(self.plat.share_deal_fee) + + if self.plat.share_deal_reduce_trades is None: + self.check_boxes_ticked[3] = False + self.share_deal_reduce_trades_check.setChecked(False) + else: + self.check_boxes_ticked[3] = True + self.share_deal_reduce_trades_check.setChecked(True) + self.share_deal_reduce_trades_box.setValue(int(self.plat.share_deal_reduce_trades)) + + if self.plat.share_deal_reduce_trades is None: + self.check_boxes_ticked[4] = False + self.share_deal_reduce_amount_check.setChecked(False) + else: + self.check_boxes_ticked[4] = True + self.share_deal_reduce_amount_check.setChecked(True) + self.share_deal_reduce_amount_box.setValue(self.plat.share_deal_reduce_amount) + + self.first_tier_box.setValue(self.plat.fund_plat_fee[0][1]) + self.first_tier_fee_box.setValue(self.plat.fund_plat_fee[1][1]) + self.add_row(loading=True) + # Handle events for field in self.required_fields: field.valueChanged.connect(self.check_valid) @@ -108,6 +150,7 @@ class PlatformEdit(QWidget): # Get fee structure variables from user input when "Save" clicked def init_variables(self): + """ # If debugging, save time by hardcoding if self.autofill: self.plat_name = "AJBell" @@ -123,14 +166,15 @@ class PlatformEdit(QWidget): self.share_deal_reduce_amount = 3.50 self.check_boxes_ticked = [True, True, True, True, True] else: - self.plat_name = self.plat_name_box.text() - self.fund_plat_fee = self.create_plat_fee_struct() - self.fund_deal_fee = float(self.fund_deal_fee_box.value()) - self.share_plat_fee = float(self.share_plat_fee_box.value()) / 100 - self.share_plat_max_fee = float(self.share_plat_max_fee_box.value()) - self.share_deal_fee = float(self.share_deal_fee_box.value()) - self.share_deal_reduce_trades = float(self.share_deal_reduce_trades_box.value()) - self.share_deal_reduce_amount = float(self.share_deal_reduce_amount_box.value()) + """ + self.plat_name = self.plat_name_box.text() + self.fund_plat_fee = self.create_plat_fee_struct() + self.fund_deal_fee = float(self.fund_deal_fee_box.value()) + self.share_plat_fee = float(self.share_plat_fee_box.value()) / 100 + self.share_plat_max_fee = float(self.share_plat_max_fee_box.value()) + self.share_deal_fee = float(self.share_deal_fee_box.value()) + self.share_deal_reduce_trades = float(self.share_deal_reduce_trades_box.value()) + self.share_deal_reduce_amount = float(self.share_deal_reduce_amount_box.value()) # Once user input is received show main window self.main_win.show() @@ -215,50 +259,63 @@ class PlatformEdit(QWidget): max_band = self.first_tier_box.value() self.val_above_lab.setText(f"on the value above £{int(max_band)} there is no charge") - def add_row(self): + def add_row(self, loading: bool = False): + if loading: + rows_needed = self.fund_fee_rows + else: + rows_needed = 1 + widgets = [] - font = QFont() - font.setPointSize(11) + for x in range(rows_needed): + font = QFont() + font.setPointSize(11) - widgets.append(QLabel(self.gridLayoutWidget_2)) - widgets[0].setFont(font) + widgets.append(QLabel(self.gridLayoutWidget_2)) + widgets[0].setFont(font) - widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) - widgets[1].setPrefix("£") - widgets[1].setMaximum(9999999) - widgets[1].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) - widgets[1].setFont(font) - widgets[1].valueChanged.connect(self.check_valid) - widgets[1].valueChanged.connect(self.update_tier_labels) + widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) + widgets[1].setPrefix("£") + widgets[1].setMaximum(9999999) + widgets[1].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) + widgets[1].setFont(font) + if loading: + widgets[1].setValue(self.plat.fund_plat_fee[0][x+1]) + widgets[1].valueChanged.connect(self.check_valid) + widgets[1].valueChanged.connect(self.update_tier_labels) - widgets.append(QLabel(self.gridLayoutWidget_2)) - widgets[2].setText(f"the fee is") - widgets[2].setFont(font) + widgets.append(QLabel(self.gridLayoutWidget_2)) + widgets[2].setText(f"the fee is") + widgets[2].setFont(font) - widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) - widgets[3].setSuffix("%") - widgets[3].setMaximum(100) - widgets[3].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) - widgets[3].setFont(font) - widgets[3].valueChanged.connect(self.check_valid) + widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) + widgets[3].setSuffix("%") + widgets[3].setMaximum(100) + widgets[3].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) + widgets[3].setFont(font) + if loading: + widgets[3].setValue(self.plat.fund_plat_fee[1][x+1]) + widgets[3].valueChanged.connect(self.check_valid) - # TODO: why 28.5? - self.gridLayoutWidget_2.setGeometry(11, 309, 611, int(round(28.5 * (self.fund_fee_rows + 1), 0))) - for i in range(len(widgets)): - self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1) + # TODO: why 28.5? + self.gridLayoutWidget_2.setGeometry(11, 309, 611, int(round(28.5 * (self.fund_fee_rows + 1), 0))) + for i in range(len(widgets)): + self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1) - self.fund_fee_rows += 1 + if not loading: + self.fund_fee_rows += 1 - self.widgets_list_list.append(widgets) - cur_label_idx = self.gridLayout_2.indexOf(widgets[0]) - cur_box_idx = self.gridLayout_2.indexOf(widgets[1]) - cur_label_pos = list(self.gridLayout_2.getItemPosition(cur_label_idx))[:2] - cur_box_pos = list(self.gridLayout_2.getItemPosition(cur_box_idx))[:2] + self.widgets_list_list.append(widgets) + cur_label_idx = self.gridLayout_2.indexOf(widgets[0]) + cur_box_idx = self.gridLayout_2.indexOf(widgets[1]) + cur_label_pos = list(self.gridLayout_2.getItemPosition(cur_label_idx))[:2] + cur_box_pos = list(self.gridLayout_2.getItemPosition(cur_box_idx))[:2] - prev_box_row = cur_box_pos[0] - 1 - prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget() - cur_label_item = self.gridLayout_2.itemAtPosition(cur_label_pos[0], cur_label_pos[1]).widget() - cur_label_item.setText(f"between £{int(prev_box_item.value())} and") + prev_box_row = cur_box_pos[0] - 1 + prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget() + #if loading: + # prev_box_item.setValue(self.plat.fund_plat_fee[0][x+1]) + cur_label_item = self.gridLayout_2.itemAtPosition(cur_label_pos[0], cur_label_pos[1]).widget() + cur_label_item.setText(f"between £{int(prev_box_item.value())} and") if self.fund_fee_rows > 1: self.del_row_but.setEnabled(True) diff --git a/src/platform_list.py b/src/platform_list.py index 91cccc9..110f0a9 100644 --- a/src/platform_list.py +++ b/src/platform_list.py @@ -4,6 +4,8 @@ from PyQt6.QtCore import QRegularExpression from PyQt6 import uic import resource_finder +import data_struct +import platform_edit class PlatformRename(QWidget): @@ -28,8 +30,11 @@ class PlatformList(QWidget): self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) self.plat_list_dialog = PlatformRename() + self.p_edit = None self.db = db self.plat_name_list = self.db.retrieve_plat_list() + self.plat_list = self.db.retrieve_platforms() + print(self.plat_list[1].fund_plat_fee) print(self.plat_name_list) for platform in self.plat_name_list: @@ -39,6 +44,29 @@ class PlatformList(QWidget): # Handle events self.add_plat_but.clicked.connect(self.add_platform) + self.del_plat_but.clicked.connect(self.remove_platform) + self.edit_plat_but.clicked.connect(self.edit_platform) + self.plat_enabled_check.checkStateChanged.connect(self.toggle_platform_state) + self.platListWidget.currentRowChanged.connect(self.get_enabled_state) def add_platform(self): self.plat_list_dialog.show() + + def get_enabled_state(self): + index = self.platListWidget.currentRow() + is_enabled = self.plat_list[index].enabled + if is_enabled: + self.plat_enabled_check.setChecked(True) + else: + self.plat_enabled_check.setChecked(False) + + def edit_platform(self): + index = self.platListWidget.currentRow() + self.p_edit = platform_edit.PlatformEdit(self.plat_list[index]) + self.p_edit.show() + + def toggle_platform_state(self): + return None + + def remove_platform(self): + return None From d8c4b5d64d042974c90a662d06210997a06a5fdf Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Tue, 22 Apr 2025 17:12:07 +0100 Subject: [PATCH 03/15] add data_struct.py file --- src/data_struct.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/data_struct.py diff --git a/src/data_struct.py b/src/data_struct.py new file mode 100644 index 0000000..6e61055 --- /dev/null +++ b/src/data_struct.py @@ -0,0 +1,16 @@ +class Platform: + def __init__(self, plat_id, fund_plat_fee, plat_name, enabled, fund_deal_fee, + share_plat_fee, share_plat_max_fee, share_deal_fee, + share_deal_reduce_trades, share_deal_reduce_amount, + ): + + self.plat_id = plat_id + self.fund_plat_fee = fund_plat_fee + self.plat_name = plat_name + self.enabled = enabled + self.fund_deal_fee = fund_deal_fee + self.share_plat_fee = share_plat_fee + self.share_plat_max_fee = share_plat_max_fee + self.share_deal_fee = share_deal_fee + self.share_deal_reduce_trades = share_deal_reduce_trades + self.share_deal_reduce_amount = share_deal_reduce_amount From 929c3719c0c0942664970a486a63d22e649574e2 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Thu, 24 Apr 2025 22:55:26 +0100 Subject: [PATCH 04/15] complete db & graph implementation (except for delete) --- SIPPCompare.db | Bin 53248 -> 45056 bytes gui/dialogs/platform_rename.ui | 70 ++++++++++++++++------ gui/output_window.ui | 103 ++++++--------------------------- gui/platform_edit.ui | 3 + gui/platform_list.ui | 22 ++++++- src/db_handler.py | 93 +++++++++++++++++++++++++++-- src/main.py | 21 ++----- src/main_window.py | 99 ++++++++++++++++--------------- src/output_window.py | 31 ++++++++-- src/platform_edit.py | 103 ++++++++++++++++++--------------- src/platform_list.py | 81 +++++++++++++++++++------- src/widgets/mpl_widget.py | 14 +++++ 12 files changed, 396 insertions(+), 244 deletions(-) create mode 100644 src/widgets/mpl_widget.py diff --git a/SIPPCompare.db b/SIPPCompare.db index ef44f752cfd449b182e401ab5439891013c26e56..0e2d14c8a24c39ba03f31c72c2299c67a47d8281 100644 GIT binary patch literal 45056 zcmeI)L2TPp7zc2_*op15-D^>`fRKt6RMF51F~QbtZI`60Tf)+`O;g0UR4#ELB28J6 zcGI|^mhQlW7$?>pKpa3~NQld%?Sz0E>d@E$fi!{OFliD)8Um?8J@B3#N6wP81vb=W zf2)!Gp7ZSI-}heX=htR(BAzLl+U$IO&M0b639SMlLPXO9L0Dz4CiZd`FWZou1X~l= zcD>D2;fcl5Ep%P*%PC>khxGI28@p!xmz&S45Btx^Ddop{1(6Q{2tWV=5V+R_mSt5A zghFH`Ry1a^W^pDvRWS2IX3@xG3zaGV;iMi-=~^l}7}vF6<<6ki<=1K~L4LT{P*9B} zQu>IV)DmMUEio03Yg38X#FXAsyVSUuD`e(#&l%YTGpMrj#>)lkl$gwB(q_I?;P#W% zEAz6EH&b~dZ5FC4Sf|YJLN5K!ii{^?qtWD1?TCJ~%i#!BU3RF??+c8Dh>*#p%@c)V zS$3;4Z7dY$t@L!I_Vk|0bkkB^w!`>{6GmRo@vKoCHhEB8DWximuAOzm$PdMDylREA zj?|!$OK&fBtd84yV}+!vMm<66Ts=X4piu4V438!C*hs>Tf~#yuOX|aVQcoP#C#^>9 z>?;Jjg3dO(>RJ&~wuqPu){2-btuDXdl>^<~G$7@=uiyfKTsKH6zgDT#*Wj}RFB+c}~i{;+OyZBb;uuDYe<`;6s&1zMznf!k( zW5DMNOmuJ3m)*g&n}#c%^3qAsY8xxv?zX`jqpNK=NvUSraQ5DXwqeD*bo^~~Tf3F; z5~Ie6a$m0mh23$EAW3moa(q*FMxELf`Ll*sO>6n685Oi82Oj-rm<)i;zq8J zob7W@pwd0RYg%z?D_vSf!?CO(6xrRKwJa_AOHafKmY;|fB*y_009Ue(H}r`N>Mh#fe{A*u=j~!mrMZUcL44hQEbP{z{}fB*y_009U<00Izz00bIIp!I2qh_a{Sjn~7MR{7EzzAfWi z>me(L4o1RP>`QMQuyVvJXTsO)nS5`*L=?qC`y^GAvZR&{gSFEv{zE>f|u-Ux{qh@f~9jxdnH9t{Pe8Q#D@9w9r_!+M&G1g(~I;Y z`W`(_({zIBjT9PO3j`nl0SG_<0uX=z1Rwwb2teRJ2|VCsLw;h9`Q6+0$U9uha*g*7 zm9ks|{sEtt4cp0npO=lw$zE0;ANvb`FM9|;JpFB6HpVBe#{hkHj_WzVK|9CwAmBio zmyP(XQl)1Ay>^c4A;3QV{r`mC6xa_I2tWV=5P$##AOHafKmY;|fB*yh48UPY~ x^ycZVKfioC`gk+{|9?U^1ondk0uX=z1Rwwb2tWV=5P$##AOL{|6c9y4`5QTB{nItolZu_j%ja6HRV5PCevhIRNS<-YnDY0qVrm5Idm~LhqWz0lk(oNbFxFXW75-q7=n-hpPx&4qnSp4(gUoaU5FPG$@} z)|MXAha*~SzHHXSZxQLKn3m4wjcFsNXXo;Ib}5t5m$K=FC1aGbExu=NSR!xAvSJ!F zDdXnyCC{2y-5iuKpG(gsbIbaSu{`X3t2m{l=5j`QI@_M6=Zut*GqM+qMLkwqDLYN9 z#{6|e;*C(h5{<{DO*3KDXV6d%2ofK;@9ve+7zN}2Znj8QZ21j-Z9JdeHqH7 zqP5XV-K*(3>FM)@9RLD!7lBS zbOxtX)?6ipyzy>cG(BB4Dxyyp_dPOS%bf~Hn_AzHd+kp#>9k%bh&oB z5<7Qj(acG+LM?3T!EUeh_Up$rdep9EG$u+NQeWnM?3L)qh_sn-6-c0^K%Ke42b4w^ z%>g3M?EZPmDNm6-<6l5}QafWE|Iu&ITuK^SyZwd^aLO{v^s$KHFOJvQb>~rgHr$i? zL>i*YwW_r{k~^|}vEGhk&z~YWXBF!gEoX>}Zs;p*Cns0e>Xn*X`Qy6bA@#iR7sAoG zxWr18qIH$NCg{YnVAgAE&HVyNc_!7u8UKFQo2SFknV|y;J7c}tEb8us=L2E!;Vn0N za_e2I*5$fYxhTFCMI)SD**EIcClAK*VS{tG=IKmY;|fB*y_009U< z00Izz00ba#0t8M=bb_xiHZf>7)_H#3KJix)t$X}K5%1ss2l!8nKcWW&2tWV=5P$## zAOHafKmY;|fB*zenm{-G$3I0%tgp=8{o~e7WdHU5#NYp`{6n$+->>{0|BipnKj$|n z0s#ULfB*y_009U<00Izz00bZafx{N)4biH8l4s#R)sFG3{HNG4o~8f7;SeqTCxv=L zw6dS%S^O_($9Pu%lkFJK@_&I)h?f7;#QDF(cWC{;!#*U`2LT8`00Izz00bZa0SG_< z0uX?}aS#ye|6%?A PlatformRename - + 0 0 300 - 90 + 100 - Rename Platform + Name Platform - + - 221 - 62 - 75 - 24 + 7 + 41 + 287 + 22 - - OK + + + 11 + - 60 - 10 - 191 + 35 + 9 + 241 20 + + + 11 + + Enter a new name for the platform - + - 8 - 34 - 287 - 22 + 220 + 70 + 75 + 24 + + + 10 + + + + OK + - + + + rename_plat_ok_but + clicked() + PlatformRename + accept() + + + 257 + 71 + + + 149 + 44 + + + + diff --git a/gui/output_window.ui b/gui/output_window.ui index 3ba39c9..50c50d1 100644 --- a/gui/output_window.ui +++ b/gui/output_window.ui @@ -1,104 +1,37 @@ - ResultsWindow - + OutputWindow + 0 0 - 400 - 355 + 1330 + 630 - - - 400 - 355 - - - - - 400 - 355 - - Results - + - 10 - 10 - 381 - 301 + -10 + -10 + 1350 + 650 - - - 11 - - - - - - - 318 - 323 - 75 - 24 - - - - - 10 - - - - OK - - - - - - 238 - 323 - 75 - 24 - - - - - 10 - - - - Save - - - output - res_save_but - res_ok_but - + + + MplWidget + QWidget +
widgets/mpl_widget
+ 1 +
+
- - - res_ok_but - clicked(bool) - ResultsWindow - close() - - - 357 - 281 - - - 199 - 149 - - - - +
diff --git a/gui/platform_edit.ui b/gui/platform_edit.ui index fe3a57d..56f5558 100644 --- a/gui/platform_edit.ui +++ b/gui/platform_edit.ui @@ -2,6 +2,9 @@ PlatformEdit + + Qt::WindowModality::ApplicationModal + 0 diff --git a/gui/platform_list.ui b/gui/platform_list.ui index fe6679a..e9d1222 100644 --- a/gui/platform_list.ui +++ b/gui/platform_list.ui @@ -2,6 +2,9 @@ PlatformList + + Qt::WindowModality::ApplicationModal + 0 @@ -120,5 +123,22 @@ - + + + plist_save_but + clicked() + PlatformList + close() + + + 196 + 453 + + + 131 + 236 + + + + diff --git a/src/db_handler.py b/src/db_handler.py index 7e95c14..252d62e 100644 --- a/src/db_handler.py +++ b/src/db_handler.py @@ -1,11 +1,13 @@ import os import sqlite3 -import data_struct + +import resource_finder from data_struct import Platform class DBHandler: def __init__(self): + # Function to create all necessary database tables if the DB file doesn't exist def create_tables(): self.cur.execute(""" CREATE TABLE "tblPlatforms" ( @@ -59,16 +61,17 @@ class DBHandler: ) """) - if not os.path.exists("SIPPCompare.db"): + if not os.path.exists(resource_finder.get_res_path("SIPPCompare.db")): db_exists = False else: db_exists = True - self.conn = sqlite3.connect("SIPPCompare.db") + self.conn = sqlite3.connect(resource_finder.get_res_path("SIPPCompare.db")) self.cur = self.conn.cursor() if not db_exists: create_tables() + # Retrieve the list of platform names for list population def retrieve_plat_list(self) -> list: res = self.cur.execute("SELECT PlatformName FROM tblPlatforms").fetchall() plat_name_list = [] @@ -77,7 +80,80 @@ class DBHandler: return plat_name_list + def write_platforms(self, plat_list: list[Platform]): + for i in range(len(plat_list)): + platforms_data = [ + plat_list[i].plat_id, + plat_list[i].plat_name, + plat_list[i].enabled + ] + + flat_plat_fees_data = [ + plat_list[i].plat_id, + plat_list[i].share_plat_fee, + plat_list[i].share_plat_max_fee + ] + + flat_deal_fees_data = [ + plat_list[i].plat_id, + plat_list[i].fund_deal_fee, + plat_list[i].share_deal_fee, + plat_list[i].share_deal_reduce_trades, + plat_list[i].share_deal_reduce_amount + ] + + res = self.cur.execute(f""" + SELECT EXISTS( + SELECT PlatformID FROM tblPlatforms + WHERE PlatformID = {i} + )""").fetchall() + + if res[0][0] == 1: + self.cur.execute(f""" + UPDATE tblPlatforms SET + PlatformID = ?, + PlatformName = ?, + IsEnabled = ? + WHERE PlatformID = {i} + """, platforms_data) + + self.cur.execute(f""" + UPDATE tblFlatPlatFees SET + PlatformID = ?, + SharePlatFee = ?, + SharePlatMaxFee = ? + WHERE PlatformID = {i} + """, flat_plat_fees_data) + + self.cur.execute(f""" + UPDATE tblFlatDealFees SET + PlatformID = ?, + FundDealFee = ?, + ShareDealFee = ?, + ShareDealReduceTrades = ?, + ShareDealReduceAmount = ? + WHERE PlatformID = {i} + """, flat_deal_fees_data) + + self.cur.execute(f"DELETE FROM tblFundPlatFee WHERE PlatformID = {i}") + else: + self.cur.execute("INSERT INTO tblPlatforms VALUES (?, ?, ?)", platforms_data) + self.cur.execute("INSERT INTO tblFlatPlatFees VALUES (?, ?, ?)", flat_plat_fees_data) + self.cur.execute("INSERT INTO tblFlatDealFees VALUES (?, ?, ?, ?, ?)", flat_deal_fees_data) + + exec_str = f"INSERT INTO tblFundPlatFee VALUES\n" + for x in range(len(plat_list[i].fund_plat_fee[0])): + band = plat_list[i].fund_plat_fee[0][x] + fee = plat_list[i].fund_plat_fee[1][x] + exec_str += f"({i}, {band}, {fee}),\n" + exec_str = exec_str[:-2] + self.cur.execute(exec_str) + + self.conn.commit() + + # Retrieve all info about all platforms in DB and initialise Platform objects def retrieve_platforms(self) -> list[Platform]: + # Retrieve all one-to-one relations platforms_res = self.cur.execute(""" SELECT -- tblPlatforms @@ -104,10 +180,13 @@ class DBHandler: platform[5], platform[6], platform[7], platform[8]] platforms.append(this_platform) + # Insert 2D array into each platform data in preparation for fund_plat_fee retrival for platform in platforms: platform.insert(1, [[], []]) - fund_plat_fee_res = self.cur.execute("SELECT * FROM tblFundPlatFee").fetchall() + # Get all records from tblFundPlatFee, add them to the platforms list based on ID + # WARNING: This code is dependent on PlatformID being sequential from 0 in DB records + fund_plat_fee_res = self.cur.execute("SELECT * FROM tblFundPlatFee ORDER BY PlatformID ASC").fetchall() for i in range(len(fund_plat_fee_res)): plat_id = fund_plat_fee_res[i][0] platforms[plat_id][1][0].append(fund_plat_fee_res[i][1]) @@ -123,15 +202,18 @@ class DBHandler: return platform_obj_list - + # This function writes the details the user entered this session to the DB def write_user_details(self, pension_val: float, slider_val: int, share_trades: int, fund_trades: int): # Hardcode UserID as 0 user_details_data = (0, pension_val, slider_val, share_trades, fund_trades) + # Check if there is already a record in tblUserDetails res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone() if res[0] == 0: + # If there isn't then insert a new record self.cur.execute("INSERT INTO tblUserDetails VALUES (?, ?, ?, ?, ?)", user_details_data) else: + # If there is then update the existing record (only ever one record as of now) self.cur.execute(""" UPDATE tblUserDetails SET UserID = ?, @@ -142,6 +224,7 @@ class DBHandler: """, user_details_data) self.conn.commit() + # Function to retrieve details entered by the user in prev session from DB def retrieve_user_details(self) -> dict: res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone() if res[0] == 0: diff --git a/src/main.py b/src/main.py index 4ee5f19..86dad41 100644 --- a/src/main.py +++ b/src/main.py @@ -1,23 +1,12 @@ -from PyQt6.QtWidgets import QApplication - import sys -import platform_edit -import main_window +from PyQt6.QtWidgets import QApplication + +from platform_edit import PlatformEdit +from main_window import SIPPCompare app = QApplication(sys.argv) -# Show platform edit window first, before main win -# When debugging, can be useful to autofill values to save time -"""if len(sys.argv) > 1: - if sys.argv[1] == "--DEBUG_AUTOFILL": - window = platform_edit.PlatformEdit(True) - else: - window = platform_edit.PlatformEdit(False) -else: - window = platform_edit.PlatformEdit(False)""" -#plat_edit_win = platform_edit.PlatformEdit() -window = main_window.SIPPCompare() - +window = SIPPCompare() window.show() app.exec() diff --git a/src/main_window.py b/src/main_window.py index f500469..8bf91de 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,17 +1,18 @@ -from PyQt6.QtGui import QIntValidator, QIcon -from PyQt6.QtWidgets import QMainWindow, QWidget from PyQt6 import uic +from PyQt6.QtGui import QIntValidator, QIcon +from PyQt6.QtWidgets import QMainWindow -import output_window -import platform_list import resource_finder -import db_handler +from db_handler import DBHandler +from output_window import OutputWindow +from platform_list import PlatformList class SIPPCompare(QMainWindow): def __init__(self): super().__init__() # Import Qt Designer UI XML file + uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) @@ -32,11 +33,12 @@ class SIPPCompare(QMainWindow): self.fund_deal_fees = 0.0 self.share_plat_fees = 0.0 self.share_deal_fees = 0.0 + self.results = [] # Create window objects - self.db = db_handler.DBHandler() - self.platform_list_win = platform_list.PlatformList(self.db) - self.output_win = output_window.OutputWindow() + self.db = DBHandler() + self.platform_list_win = PlatformList(self.db) + self.output_win = OutputWindow() # Handle events self.calc_but.clicked.connect(self.calculate_fees) @@ -61,8 +63,6 @@ class SIPPCompare(QMainWindow): self.fund_trades_combo.setCurrentText(str(prev_session_data["fund_trades"])) self.calc_but.setFocus() - - # Display slider position as mix between two nums (funds/shares) def update_slider_lab(self): slider_val = self.mix_slider.value() @@ -78,6 +78,7 @@ class SIPPCompare(QMainWindow): self.calc_but.setEnabled(False) # Get variables from platform editor input fields + """ def init_variables(self): self.optional_boxes = self.platform_win.get_optional_boxes() self.fund_plat_fee = self.platform_win.get_fund_plat_fee() @@ -109,50 +110,59 @@ class SIPPCompare(QMainWindow): self.share_deal_reduce_amount = self.platform_win.get_share_deal_reduce_amount() else: self.share_deal_reduce_amount = None + """ + def init_variables(self): + self.fund_plat_fee = 1 # Calculate fees def calculate_fees(self): - self.init_variables() - - # Set to zero each time to avoid persistence - self.fund_plat_fees = 0 + # Set to empty list each time to avoid persistence + self.results = [] # Get user input value_num = float(self.value_input.value()) slider_val: int = self.mix_slider.value() fund_trades_num = int(self.fund_trades_combo.currentText()) share_trades_num = int(self.share_trades_combo.currentText()) - - # Funds/shares mix funds_value = (slider_val / 100) * value_num - if self.fund_deal_fee is not None: - self.fund_deal_fees = fund_trades_num * self.fund_deal_fee - - for i in range(1, len(self.fund_plat_fee[0])): - band = self.fund_plat_fee[0][i] - prev_band = self.fund_plat_fee[0][i - 1] - fee = self.fund_plat_fee[1][i] - gap = (band - prev_band) - - if funds_value > gap: - self.fund_plat_fees += gap * (fee / 100) - funds_value -= gap - else: - self.fund_plat_fees += funds_value * (fee / 100) - break - shares_value = (1 - (slider_val / 100)) * value_num - if self.share_plat_max_fee is not None: - if (self.share_plat_fee * shares_value / 12) > self.share_plat_max_fee: - self.share_plat_fees = self.share_plat_max_fee * 12 - else: - self.share_plat_fees = self.share_plat_fee * shares_value - if self.share_deal_reduce_trades is not None: - if (share_trades_num / 12) >= self.share_deal_reduce_trades: - self.share_deal_fees = self.share_deal_reduce_amount * share_trades_num - else: - self.share_deal_fees = self.share_deal_fee * share_trades_num + for platform in self.platform_list_win.plat_list: + fund_plat_fees = 0.0 + fund_deal_fees = 0.0 + share_plat_fees = 0.0 + share_deal_fees = 0.0 + plat_name = platform.plat_name + + if platform.fund_deal_fee is not None: + fund_deal_fees = fund_trades_num * platform.fund_deal_fee + + for i in range(1, len(platform.fund_plat_fee[0])): + band = platform.fund_plat_fee[0][i] + prev_band = platform.fund_plat_fee[0][i - 1] + fee = platform.fund_plat_fee[1][i] + gap = (band - prev_band) + + if funds_value > gap: + fund_plat_fees += gap * (fee / 100) + funds_value -= gap + else: + fund_plat_fees += funds_value * (fee / 100) + break + + if platform.share_plat_max_fee is not None: + if (platform.share_plat_fee * shares_value / 12) > platform.share_plat_max_fee: + share_plat_fees = platform.share_plat_max_fee * 12 + else: + share_plat_fees = platform.share_plat_fee * shares_value + + if platform.share_deal_reduce_trades is not None: + if (share_trades_num / 12) >= platform.share_deal_reduce_trades: + share_deal_fees = platform.share_deal_reduce_amount * share_trades_num + else: + share_deal_fees = platform.share_deal_fee * share_trades_num + + self.results.append([fund_plat_fees, fund_deal_fees, share_plat_fees, share_deal_fees, plat_name]) self.db.write_user_details(value_num, slider_val, share_trades_num, fund_trades_num) self.show_output_win() @@ -160,10 +170,7 @@ class SIPPCompare(QMainWindow): # Show the output window - this func is called from calculate_fee() def show_output_win(self): # Refresh the results when new fees are calculated - self.output_win.display_output(self.fund_plat_fees, self.fund_deal_fees, - self.share_plat_fees, self.share_deal_fees, - self.plat_name - ) + self.output_win.display_output(self.results) self.output_win.show() def show_platform_list(self): diff --git a/src/output_window.py b/src/output_window.py index 06933da..fe80f64 100644 --- a/src/output_window.py +++ b/src/output_window.py @@ -1,10 +1,10 @@ -from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QWidget -from PyQt6 import uic - import datetime import os +from PyQt6 import uic +from PyQt6.QtGui import QIcon +from PyQt6.QtWidgets import QWidget + import resource_finder @@ -15,6 +15,7 @@ class OutputWindow(QWidget): uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + """ # Initialise class variables self.results_str = "" self.platform_name = "" @@ -62,3 +63,25 @@ class OutputWindow(QWidget): self.results_str += f"\n\nTotal fees: £{round(total_fees, 2):.2f}" self.output.setText(self.results_str) + """ + + def display_output(self, results: list): + self.graphWidget.canvas.axes.clear() + self.graphWidget.canvas.axes.cla() + self.graphWidget.canvas.draw_idle() + ax = self.graphWidget.canvas.axes + #self.graphWidget.clf() + names = [] + values = [] + for result in results: + names.append(result[4]) + values.append(sum(result[:4])) + h_bars = ax.barh(names, values) + #labels = [] + #for value in values: + # labels.append(f"£{str(value)}") + + ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues]) + #ax.draw() + #self.graphWidget.draw() + diff --git a/src/platform_edit.py b/src/platform_edit.py index 07b8817..cf45221 100644 --- a/src/platform_edit.py +++ b/src/platform_edit.py @@ -1,12 +1,12 @@ -from PyQt6.QtCore import QRegularExpression +from PyQt6 import uic +from PyQt6.QtCore import QRegularExpression, QRect from PyQt6.QtGui import QRegularExpressionValidator, QFont, QIcon from PyQt6.QtWidgets import QWidget, QLabel -from PyQt6 import uic -from widgets.fastedit_spinbox import FastEditQDoubleSpinBox -from data_struct import Platform -import main_window import resource_finder +from db_handler import DBHandler +from data_struct import Platform +from widgets.fastedit_spinbox import FastEditQDoubleSpinBox class PlatformEdit(QWidget): @@ -20,14 +20,10 @@ class PlatformEdit(QWidget): self.plat = plat self.fund_plat_fee = self.plat.fund_plat_fee self.widgets_list_list = [] - - self.fund_fee_rows = len(self.plat.fund_plat_fee[0]) - """ - # Debugging feature: set with "--DEBUG_AUTOFILL" cmd argument - self.autofill = autofill - if autofill: - self.save_but.setEnabled(True) - """ + if len(self.plat.fund_plat_fee[0]) > 1: + self.fund_fee_rows = len(self.plat.fund_plat_fee[0]) - 1 + else: + self.fund_fee_rows = 1 self.required_fields = [ self.share_plat_fee_box, @@ -58,6 +54,7 @@ class PlatformEdit(QWidget): False ] + # Set optional checkboxes based on DB storage if self.plat.plat_name is None: self.check_boxes_ticked[0] = False self.plat_name_check.setChecked(False) @@ -102,8 +99,10 @@ class PlatformEdit(QWidget): self.share_deal_reduce_amount_check.setChecked(True) self.share_deal_reduce_amount_box.setValue(self.plat.share_deal_reduce_amount) - self.first_tier_box.setValue(self.plat.fund_plat_fee[0][1]) - self.first_tier_fee_box.setValue(self.plat.fund_plat_fee[1][1]) + # Populate fund platform fee rows from DB + if len(self.plat.fund_plat_fee[0]) > 1: + self.first_tier_box.setValue(self.plat.fund_plat_fee[0][1]) + self.first_tier_fee_box.setValue(self.plat.fund_plat_fee[1][1]) self.add_row(loading=True) # Handle events @@ -150,34 +149,34 @@ class PlatformEdit(QWidget): # Get fee structure variables from user input when "Save" clicked def init_variables(self): - """ - # If debugging, save time by hardcoding - if self.autofill: - self.plat_name = "AJBell" - self.fund_plat_fee = [ - [0, 250000, 1000000, 2000000], - [0, 0.25, 0.1, 0.05] - ] - self.fund_deal_fee = 1.50 - self.share_plat_fee = 0.0025 - self.share_plat_max_fee = 3.50 - self.share_deal_fee = 5.00 - self.share_deal_reduce_trades = 10 - self.share_deal_reduce_amount = 3.50 - self.check_boxes_ticked = [True, True, True, True, True] - else: - """ - self.plat_name = self.plat_name_box.text() - self.fund_plat_fee = self.create_plat_fee_struct() - self.fund_deal_fee = float(self.fund_deal_fee_box.value()) - self.share_plat_fee = float(self.share_plat_fee_box.value()) / 100 - self.share_plat_max_fee = float(self.share_plat_max_fee_box.value()) - self.share_deal_fee = float(self.share_deal_fee_box.value()) - self.share_deal_reduce_trades = float(self.share_deal_reduce_trades_box.value()) - self.share_deal_reduce_amount = float(self.share_deal_reduce_amount_box.value()) + self.plat.fund_plat_fee = self.create_plat_fee_struct() + self.plat.share_plat_fee = float(self.share_plat_fee_box.value()) / 100 + self.plat.share_deal_fee = float(self.share_deal_fee_box.value()) - # Once user input is received show main window - self.main_win.show() + if self.check_boxes_ticked[0]: + self.plat.plat_name = self.plat_name_box.text() + else: + self.plat.plat_name = None + + if self.check_boxes_ticked[1]: + self.plat.fund_deal_fee = float(self.fund_deal_fee_box.value()) + else: + self.plat.fund_deal_fee = None + + if self.check_boxes_ticked[2]: + self.plat.share_plat_max_fee = float(self.share_plat_max_fee_box.value()) + else: + self.plat.share_plat_max_fee = None + + if self.check_boxes_ticked[3]: + self.plat.share_deal_reduce_trades = int(self.share_deal_reduce_trades_box.value()) + else: + self.plat.share_deal_reduce_trades = None + + if self.check_boxes_ticked[4]: + self.plat.share_deal_reduce_amount = float(self.share_deal_reduce_amount_box.value()) + else: + self.plat.share_deal_reduce_amount = None # This method does multiple things in order to validate the user's inputs: # 1) Check all required fields have a non-zero value @@ -261,12 +260,12 @@ class PlatformEdit(QWidget): def add_row(self, loading: bool = False): if loading: - rows_needed = self.fund_fee_rows + rows_needed = self.fund_fee_rows - 1 else: rows_needed = 1 - widgets = [] for x in range(rows_needed): + widgets = [] font = QFont() font.setPointSize(11) @@ -279,7 +278,7 @@ class PlatformEdit(QWidget): widgets[1].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) widgets[1].setFont(font) if loading: - widgets[1].setValue(self.plat.fund_plat_fee[0][x+1]) + widgets[1].setValue(self.plat.fund_plat_fee[0][x+2]) widgets[1].valueChanged.connect(self.check_valid) widgets[1].valueChanged.connect(self.update_tier_labels) @@ -293,13 +292,20 @@ class PlatformEdit(QWidget): widgets[3].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) widgets[3].setFont(font) if loading: - widgets[3].setValue(self.plat.fund_plat_fee[1][x+1]) + widgets[3].setValue(self.plat.fund_plat_fee[1][x+2]) widgets[3].valueChanged.connect(self.check_valid) # TODO: why 28.5? - self.gridLayoutWidget_2.setGeometry(11, 309, 611, int(round(28.5 * (self.fund_fee_rows + 1), 0))) + if loading: + grid_height = int(round(28.5 * self.fund_fee_rows)) + else: + grid_height = int(round(28.5 * (self.fund_fee_rows + 1))) + self.gridLayoutWidget_2.setGeometry(QRect(11, 309, 611, grid_height)) for i in range(len(widgets)): - self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1) + if loading: + self.gridLayout_2.addWidget(widgets[i], x + 1, i, 1, 1) + else: + self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1) if not loading: self.fund_fee_rows += 1 @@ -324,6 +330,7 @@ class PlatformEdit(QWidget): self.add_row_but.setEnabled(False) self.check_valid() + self.update_tier_labels() # TODO: Tab order diff --git a/src/platform_list.py b/src/platform_list.py index 110f0a9..93e8f9c 100644 --- a/src/platform_list.py +++ b/src/platform_list.py @@ -1,56 +1,94 @@ -from PyQt6.QtWidgets import QWidget, QListWidgetItem -from PyQt6.QtGui import QIcon, QRegularExpressionValidator -from PyQt6.QtCore import QRegularExpression from PyQt6 import uic +from PyQt6.QtCore import QRegularExpression +from PyQt6.QtGui import QIcon, QRegularExpressionValidator +from PyQt6.QtWidgets import QWidget, QListWidgetItem, QDialog import resource_finder -import data_struct -import platform_edit +from db_handler import DBHandler +from data_struct import Platform +from platform_edit import PlatformEdit -class PlatformRename(QWidget): - def __init__(self): - super().__init__() +class PlatformRename(QDialog): + def __init__(self, parent=None): + super().__init__(parent) # Import Qt Designer UI XML file uic.loadUi(resource_finder.get_res_path("gui/dialogs/platform_rename.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + self.rename_plat_box.setFocus() + self.new_name = "" + # Set validators # Regex accepts any characters that match [a-Z], [0-9] or _ self.rename_plat_box.setValidator( QRegularExpressionValidator(QRegularExpression("\\w*")) ) + self.rename_plat_ok_but.clicked.connect(self.store_new_name) + + def store_new_name(self): + self.new_name = self.rename_plat_box.text() + + def closeEvent(self, event): + event.ignore() + self.reject() + class PlatformList(QWidget): - def __init__(self, db): + def __init__(self, db: DBHandler): super().__init__() # Import Qt Designer UI XML file uic.loadUi(resource_finder.get_res_path("gui/platform_list.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) - self.plat_list_dialog = PlatformRename() - self.p_edit = None self.db = db - self.plat_name_list = self.db.retrieve_plat_list() - self.plat_list = self.db.retrieve_platforms() - print(self.plat_list[1].fund_plat_fee) - print(self.plat_name_list) + self.plat_edit_win = None + self.plat_list_dialog = PlatformRename() + self.plat_list = [] + self.plat_name_list = [] + self.new_plat_name = "" + self.update_plat_list() - for platform in self.plat_name_list: + for i in range(len(self.plat_name_list)): + plat_name = self.plat_name_list[i] item = QListWidgetItem() - item.setText(platform) + if plat_name is not None: + item.setText(plat_name) + else: + item.setText(f"Unnamed [ID: {i}]") + self.platListWidget.addItem(item) # Handle events self.add_plat_but.clicked.connect(self.add_platform) self.del_plat_but.clicked.connect(self.remove_platform) self.edit_plat_but.clicked.connect(self.edit_platform) + self.plist_save_but.clicked.connect(self.save_platforms) self.plat_enabled_check.checkStateChanged.connect(self.toggle_platform_state) self.platListWidget.currentRowChanged.connect(self.get_enabled_state) + def update_plat_list(self): + self.plat_name_list = self.db.retrieve_plat_list() + self.plat_list = self.db.retrieve_platforms() + def add_platform(self): - self.plat_list_dialog.show() + name_dialog_res = self.plat_list_dialog.exec() + if name_dialog_res == QDialog.DialogCode.Accepted: + name = self.plat_list_dialog.new_name + index = self.platListWidget.count() + if name != "": + self.platListWidget.addItem(name) + name_param = name + else: + self.platListWidget.addItem(f"Unnamed [ID: {index}]") + name_param = None + + self.plat_list.append(Platform( + index, [[0], [0]], name_param, True, 0, 0, None, 0, None, None) + ) + self.plat_edit_win = PlatformEdit(self.plat_list[index]) + self.plat_edit_win.show() def get_enabled_state(self): index = self.platListWidget.currentRow() @@ -62,8 +100,11 @@ class PlatformList(QWidget): def edit_platform(self): index = self.platListWidget.currentRow() - self.p_edit = platform_edit.PlatformEdit(self.plat_list[index]) - self.p_edit.show() + self.plat_edit_win = PlatformEdit(self.plat_list[index]) + self.plat_edit_win.show() + + def save_platforms(self): + self.db.write_platforms(self.plat_list) def toggle_platform_state(self): return None diff --git a/src/widgets/mpl_widget.py b/src/widgets/mpl_widget.py new file mode 100644 index 0000000..1d6e664 --- /dev/null +++ b/src/widgets/mpl_widget.py @@ -0,0 +1,14 @@ +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg +from matplotlib.figure import Figure +from PyQt6.QtWidgets import QWidget, QVBoxLayout + + +class MplWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.canvas = FigureCanvasQTAgg(Figure(figsize=(10, 10), dpi=100)) + vertical_layout = QVBoxLayout() + #vertical_layout.setSpacing(0) + vertical_layout.addWidget(self.canvas) + self.canvas.axes = self.canvas.figure.add_subplot(1, 1, 1) + self.setLayout(vertical_layout) \ No newline at end of file From 54b7bdd700268f6ad7caf91290d366c936f13d0d Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Fri, 25 Apr 2025 11:05:00 +0100 Subject: [PATCH 05/15] clean up files --- src/db_handler.py | 5 +++++ src/main.py | 1 - src/main_window.py | 37 ------------------------------------- src/output_window.py | 16 +++++----------- src/platform_edit.py | 31 +------------------------------ src/platform_list.py | 5 +++-- src/widgets/mpl_widget.py | 1 - 7 files changed, 14 insertions(+), 82 deletions(-) diff --git a/src/db_handler.py b/src/db_handler.py index 252d62e..dbbd5c8 100644 --- a/src/db_handler.py +++ b/src/db_handler.py @@ -241,3 +241,8 @@ class DBHandler: } return user_details_dict + + def toggle_platform_state(self, index: int, state: bool): + state_data = [state, index] + self.cur.execute("UPDATE tblPlatforms SET IsEnabled = ? WHERE PlatformID = ?", state_data) + self.conn.commit() diff --git a/src/main.py b/src/main.py index 86dad41..2517bfe 100644 --- a/src/main.py +++ b/src/main.py @@ -2,7 +2,6 @@ import sys from PyQt6.QtWidgets import QApplication -from platform_edit import PlatformEdit from main_window import SIPPCompare diff --git a/src/main_window.py b/src/main_window.py index 8bf91de..e32508d 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -77,43 +77,6 @@ class SIPPCompare(QMainWindow): else: self.calc_but.setEnabled(False) - # Get variables from platform editor input fields - """ - def init_variables(self): - self.optional_boxes = self.platform_win.get_optional_boxes() - self.fund_plat_fee = self.platform_win.get_fund_plat_fee() - self.share_plat_fee = self.platform_win.get_share_plat_fee() - self.share_deal_fee = self.platform_win.get_share_deal_fee() - - # TODO: This is HORRIBLE - find better way of doing it! (maybe enums?) - if self.optional_boxes[0]: - self.plat_name = self.platform_win.get_plat_name() - else: - self.plat_name = None - - if self.optional_boxes[1]: - self.fund_deal_fee = self.platform_win.get_fund_deal_fee() - else: - self.fund_deal_fee = None - - if self.optional_boxes[2]: - self.share_plat_max_fee = self.platform_win.get_share_plat_max_fee() - else: - self.share_plat_max_fee = None - - if self.optional_boxes[3]: - self.share_deal_reduce_trades = self.platform_win.get_share_deal_reduce_trades() - else: - self.share_deal_reduce_trades = None - - if self.optional_boxes[4]: - self.share_deal_reduce_amount = self.platform_win.get_share_deal_reduce_amount() - else: - self.share_deal_reduce_amount = None - """ - def init_variables(self): - self.fund_plat_fee = 1 - # Calculate fees def calculate_fees(self): # Set to empty list each time to avoid persistence diff --git a/src/output_window.py b/src/output_window.py index fe80f64..49014b0 100644 --- a/src/output_window.py +++ b/src/output_window.py @@ -66,22 +66,16 @@ class OutputWindow(QWidget): """ def display_output(self, results: list): - self.graphWidget.canvas.axes.clear() - self.graphWidget.canvas.axes.cla() - self.graphWidget.canvas.draw_idle() ax = self.graphWidget.canvas.axes - #self.graphWidget.clf() + ax.clear() + ax.cla() + self.graphWidget.canvas.draw_idle() + names = [] values = [] for result in results: names.append(result[4]) values.append(sum(result[:4])) + h_bars = ax.barh(names, values) - #labels = [] - #for value in values: - # labels.append(f"£{str(value)}") - ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues]) - #ax.draw() - #self.graphWidget.draw() - diff --git a/src/platform_edit.py b/src/platform_edit.py index cf45221..b625550 100644 --- a/src/platform_edit.py +++ b/src/platform_edit.py @@ -295,7 +295,6 @@ class PlatformEdit(QWidget): widgets[3].setValue(self.plat.fund_plat_fee[1][x+2]) widgets[3].valueChanged.connect(self.check_valid) - # TODO: why 28.5? if loading: grid_height = int(round(28.5 * self.fund_fee_rows)) else: @@ -332,7 +331,7 @@ class PlatformEdit(QWidget): self.check_valid() self.update_tier_labels() - # TODO: Tab order + # TODO: Tab/focus order def remove_row(self): for widget in self.widgets_list_list[self.fund_fee_rows - 2]: @@ -350,31 +349,3 @@ class PlatformEdit(QWidget): self.check_valid() self.update_tier_labels() - - # Getter functions (is this necessary? maybe directly reading class vars would be best...) - def get_optional_boxes(self): - return self.check_boxes_ticked - - def get_plat_name(self): - return self.plat_name - - def get_fund_plat_fee(self): - return self.fund_plat_fee - - def get_fund_deal_fee(self): - return self.fund_deal_fee - - def get_share_plat_fee(self): - return self.share_plat_fee - - def get_share_plat_max_fee(self): - return self.share_plat_max_fee - - def get_share_deal_fee(self): - return self.share_deal_fee - - def get_share_deal_reduce_trades(self): - return self.share_deal_reduce_trades - - def get_share_deal_reduce_amount(self): - return self.share_deal_reduce_amount diff --git a/src/platform_list.py b/src/platform_list.py index 93e8f9c..63a3c1f 100644 --- a/src/platform_list.py +++ b/src/platform_list.py @@ -34,7 +34,6 @@ class PlatformRename(QDialog): event.ignore() self.reject() - class PlatformList(QWidget): def __init__(self, db: DBHandler): super().__init__() @@ -107,7 +106,9 @@ class PlatformList(QWidget): self.db.write_platforms(self.plat_list) def toggle_platform_state(self): - return None + index = self.platListWidget.currentRow() + state = self.plat_enabled_check.isChecked() + self.db.toggle_platform_state(index, state) def remove_platform(self): return None diff --git a/src/widgets/mpl_widget.py b/src/widgets/mpl_widget.py index 1d6e664..d7c03ee 100644 --- a/src/widgets/mpl_widget.py +++ b/src/widgets/mpl_widget.py @@ -8,7 +8,6 @@ class MplWidget(QWidget): super().__init__(parent) self.canvas = FigureCanvasQTAgg(Figure(figsize=(10, 10), dpi=100)) vertical_layout = QVBoxLayout() - #vertical_layout.setSpacing(0) vertical_layout.addWidget(self.canvas) self.canvas.axes = self.canvas.figure.add_subplot(1, 1, 1) self.setLayout(vertical_layout) \ No newline at end of file From 348b027ede3e5df319f5e7b4b9f2871673ff0449 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Fri, 25 Apr 2025 11:09:55 +0100 Subject: [PATCH 06/15] remove redundant text-based output --- src/output_window.py | 50 -------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/src/output_window.py b/src/output_window.py index 49014b0..bee2fe0 100644 --- a/src/output_window.py +++ b/src/output_window.py @@ -15,56 +15,6 @@ class OutputWindow(QWidget): uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) - """ - # Initialise class variables - self.results_str = "" - self.platform_name = "" - - # Handle events - self.res_save_but.clicked.connect(self.save_results) - - def save_results(self): - # Use datatime for txt filename - cur_time = datetime.datetime.now() - if not os.path.exists("output"): - os.makedirs("output") - filename_str = f"output/" - if self.platform_name is not None: - filename_str += f"{self.platform_name}" - else: - filename_str += "Unnamed" - filename_str += f"-{cur_time.year}.{cur_time.month}.{cur_time.day}.txt" - output_file = open(filename_str, "wt", encoding = "utf-8") - output_file.write(self.results_str) - - # Display fees in output window as plaintext readout - def display_output(self, fund_plat_fees: float, fund_deal_fees: float, - share_plat_fees: float, share_deal_fees: float, plat_name: str): - self.platform_name = plat_name - if self.platform_name is not None: - self.results_str = f"Fees breakdown (Platform \"{self.platform_name}\"):" - else: - self.results_str = f"Fees breakdown:" - - self.results_str += "\n\nPlatform fees:" - # :.2f is used in order to display 2 decimal places (currency form) - self.results_str += f"\n\tFund platform fees: £{round(fund_plat_fees, 2):.2f}" - self.results_str += f"\n\tShare platform fees: £{round(share_plat_fees, 2):.2f}" - total_plat_fees = fund_plat_fees + share_plat_fees - self.results_str += f"\n\tTotal platform fees: £{round(total_plat_fees, 2):.2f}" - - self.results_str += "\n\nDealing fees:" - self.results_str += f"\n\tFund dealing fees: £{round(fund_deal_fees, 2):.2f}" - self.results_str += f"\n\tShare dealing fees: £{round(share_deal_fees, 2):.2f}" - total_deal_fees = fund_deal_fees + share_deal_fees - self.results_str += f"\n\tTotal dealing fees: £{round(total_deal_fees, 2):.2f}" - - total_fees = total_plat_fees + total_deal_fees - self.results_str += f"\n\nTotal fees: £{round(total_fees, 2):.2f}" - - self.output.setText(self.results_str) - """ - def display_output(self, results: list): ax = self.graphWidget.canvas.axes ax.clear() From f2a073597238fd0d1d9b5d258a5e1dfa24745bad Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Fri, 25 Apr 2025 17:35:54 +0100 Subject: [PATCH 07/15] add time slider and saving functionality to output --- gui/output_window.ui | 81 ++++++++++++++++++++++++++++++++++++++- src/main_window.py | 2 +- src/output_window.py | 47 ++++++++++++++++++----- src/widgets/mpl_widget.py | 4 +- 4 files changed, 120 insertions(+), 14 deletions(-) diff --git a/gui/output_window.ui b/gui/output_window.ui index 50c50d1..9e3fac9 100644 --- a/gui/output_window.ui +++ b/gui/output_window.ui @@ -7,7 +7,7 @@ 0 0 1330 - 630 + 685
@@ -23,6 +23,85 @@ + + + + 1231 + 642 + 91 + 31 + + + + + 10 + + + + Save graph + + + + + + 370 + 635 + 581 + 41 + + + + 1 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksAbove + + + 1 + + + + + + 1134 + 642 + 91 + 31 + + + + + 10 + + + + Save CSV + + + + + + 10 + 640 + 341 + 31 + + + + + 11 + + + + Fees over 1 year(s) (assuming no change in value) + + diff --git a/src/main_window.py b/src/main_window.py index e32508d..e775f48 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -133,7 +133,7 @@ class SIPPCompare(QMainWindow): # Show the output window - this func is called from calculate_fee() def show_output_win(self): # Refresh the results when new fees are calculated - self.output_win.display_output(self.results) + self.output_win.display_output(self.results, 1) self.output_win.show() def show_platform_list(self): diff --git a/src/output_window.py b/src/output_window.py index bee2fe0..f8dbdcb 100644 --- a/src/output_window.py +++ b/src/output_window.py @@ -1,11 +1,12 @@ -import datetime import os +from datetime import datetime from PyQt6 import uic from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QWidget +from PyQt6.QtWidgets import QWidget, QFileDialog import resource_finder +from widgets.mpl_widget import MplWidget class OutputWindow(QWidget): @@ -15,17 +16,43 @@ class OutputWindow(QWidget): uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) - def display_output(self, results: list): - ax = self.graphWidget.canvas.axes - ax.clear() - ax.cla() - self.graphWidget.canvas.draw_idle() + # Define class variables + self.canvas = self.graphWidget.canvas + self.ax = self.canvas.axes + self.fig = self.canvas.figure + self.results = [] + + # Handle events + self.save_graph_but.clicked.connect(self.save_graph) + self.time_slider.valueChanged.connect(self.change_time) + + def display_output(self, results: list, years: int): + self.results = results + self.ax.clear() + self.ax.cla() + self.canvas.draw_idle() names = [] values = [] for result in results: names.append(result[4]) - values.append(sum(result[:4])) + values.append(sum(result[:4]) * years) - h_bars = ax.barh(names, values) - ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues]) + h_bars = self.ax.barh(names, values) + self.ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues]) + + def save_graph(self): + file_picker = QFileDialog(self) + file_picker.setFileMode(QFileDialog.FileMode.Directory) + folder_path = "" + if file_picker.exec(): + folder_path = file_picker.selectedFiles()[0] + + cur_time = datetime.now() + filename_str = f"{folder_path}/SIPPCompare-{cur_time.year}.{cur_time.month}.{cur_time.day}.png" + self.fig.savefig(filename_str, dpi=150) + + def change_time(self): + years: int = self.time_slider.value() + self.time_lab.setText(f"Fees over {years} year(s) (assuming no change in value)") + self.display_output(self.results, years) diff --git a/src/widgets/mpl_widget.py b/src/widgets/mpl_widget.py index d7c03ee..a5de8ac 100644 --- a/src/widgets/mpl_widget.py +++ b/src/widgets/mpl_widget.py @@ -6,8 +6,8 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout class MplWidget(QWidget): def __init__(self, parent=None): super().__init__(parent) - self.canvas = FigureCanvasQTAgg(Figure(figsize=(10, 10), dpi=100)) + self.canvas = FigureCanvasQTAgg(Figure(figsize=(15, 8), dpi=100)) vertical_layout = QVBoxLayout() vertical_layout.addWidget(self.canvas) self.canvas.axes = self.canvas.figure.add_subplot(1, 1, 1) - self.setLayout(vertical_layout) \ No newline at end of file + self.setLayout(vertical_layout) From c151b19a3cb6a51de8ab22e4ac92c5398def83b8 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Fri, 25 Apr 2025 17:36:13 +0100 Subject: [PATCH 08/15] update pyinstaller spec --- SIPPCompare.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SIPPCompare.spec b/SIPPCompare.spec index 3f784b7..4322e84 100644 --- a/SIPPCompare.spec +++ b/SIPPCompare.spec @@ -5,7 +5,7 @@ a = Analysis( ['src\\main.py'], pathex=[], binaries=[], - datas=[('gui/*.ui', 'gui'), ('icon2.ico', '.')], + datas=[('gui/*.ui', 'gui'), ('gui/dialogs/*.ui', 'gui/dialogs'), ('icon2.ico', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, From 16802648bcdc332825bb280af37f74b0a52552e9 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Sat, 26 Apr 2025 12:57:49 +0100 Subject: [PATCH 09/15] implement deleting platforms and csv saving --- gui/platform_edit.ui | 22 ++++++++++++-- src/db_handler.py | 17 +++++++++++ src/output_window.py | 69 +++++++++++++++++++++++++++++++++++++++----- src/platform_edit.py | 4 +-- src/platform_list.py | 59 ++++++++++++++++++++++++++++--------- 5 files changed, 146 insertions(+), 25 deletions(-) diff --git a/gui/platform_edit.ui b/gui/platform_edit.ui index 56f5558..df3d028 100644 --- a/gui/platform_edit.ui +++ b/gui/platform_edit.ui @@ -218,7 +218,7 @@ - Fund dealing fee* + Fund dealing fee @@ -342,7 +342,7 @@ 8 - 262 + 540 191 21 @@ -508,6 +508,24 @@ on the value above £ there is no charge + + + + 10 + 284 + 151 + 16 + + + + + 11 + + + + Fund platform fee* + + diff --git a/src/db_handler.py b/src/db_handler.py index dbbd5c8..3543e1e 100644 --- a/src/db_handler.py +++ b/src/db_handler.py @@ -246,3 +246,20 @@ class DBHandler: state_data = [state, index] self.cur.execute("UPDATE tblPlatforms SET IsEnabled = ? WHERE PlatformID = ?", state_data) self.conn.commit() + + def remove_platform(self, index: int): + tbl_list = ["tblPlatforms", "tblFlatPlatFees", "tblFlatDealFees", "tblFundPlatFee"] + for tbl in tbl_list: + self.cur.execute(f"DELETE FROM {tbl} WHERE PlatformID = {index}") + + res = self.cur.execute("SELECT PlatformID from tblPlatforms").fetchall() + n = len(res) + for i in range(n): + for tbl in tbl_list: + self.cur.execute(f""" + UPDATE {tbl} + SET PlatformID = {index + i} + WHERE PlatformID = {index + 1 + i} + """) + + self.conn.commit() diff --git a/src/output_window.py b/src/output_window.py index f8dbdcb..39a20e1 100644 --- a/src/output_window.py +++ b/src/output_window.py @@ -24,6 +24,7 @@ class OutputWindow(QWidget): # Handle events self.save_graph_but.clicked.connect(self.save_graph) + self.save_csv_but.clicked.connect(self.save_csv) self.time_slider.valueChanged.connect(self.change_time) def display_output(self, results: list, years: int): @@ -43,14 +44,68 @@ class OutputWindow(QWidget): def save_graph(self): file_picker = QFileDialog(self) - file_picker.setFileMode(QFileDialog.FileMode.Directory) - folder_path = "" - if file_picker.exec(): - folder_path = file_picker.selectedFiles()[0] - + file_picker.setFileMode(QFileDialog.FileMode.AnyFile) + file_picker.setDefaultSuffix("png") + file_picker.setWindowTitle("Save results as PNG") + file_picker.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) + file_picker.setNameFilter("*.png") + file_path = "" cur_time = datetime.now() - filename_str = f"{folder_path}/SIPPCompare-{cur_time.year}.{cur_time.month}.{cur_time.day}.png" - self.fig.savefig(filename_str, dpi=150) + filename_str = f"{file_path}/SIPPCompare-{cur_time.year}.{cur_time.month}.{cur_time.day}.png" + file_picker.selectFile(filename_str) + if file_picker.exec(): + file_path = file_picker.selectedFiles()[0] + + try: + self.fig.savefig(file_path, dpi=150) + except: + pass + + def save_csv(self): + file_picker = QFileDialog(self) + file_picker.setFileMode(QFileDialog.FileMode.AnyFile) + file_picker.setDefaultSuffix("csv") + file_picker.setWindowTitle("Save results as CSV") + file_picker.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) + file_picker.setNameFilter("*.csv") + file_path = "" + cur_time = datetime.now() + filename_str = f"{file_path}/SIPPCompare-{cur_time.year}.{cur_time.month}.{cur_time.day}.csv" + file_picker.selectFile(filename_str) + if file_picker.exec(): + file_path = file_picker.selectedFiles()[0] + + try: + csvfile = open(file_path, "wt") + csv_string = ( + "Platform Name,Fund Platform Fee,Share Platform Fee,Fund Dealing Fee," + "Share Dealing Fee,Total Platform Fees,Total Dealing Fees,Total Fund Fees," + "Total Share Fees,Total Fees" + ) + + for result in self.results: + csv_string += '\n' + pn = result[4] + fpf = result[0] + spf = result[2] + fdf = result[1] + sdf = result[3] + + tpf = fpf + spf + tdf = sdf + fdf + + tff = fpf + fdf + tsf = spf + sdf + tf = tff + tsf + csv_string += ( + f"{pn},\"£{fpf:,.2f}\",\"£{spf:,.2f}\",\"£{fdf:,.2f}\",\"£{sdf:,.2f}\"," + f"\"£{tpf:,.2f}\",\"£{tdf:,.2f}\",\"£{tff:,.2f}\",\"£{tsf:,.2f}\",\"£{tf:,.2f}\"" + ) + + csvfile.write(csv_string) + csvfile.close() + except OSError: + print("ERROR FILE SAVE FAILED") def change_time(self): years: int = self.time_slider.value() diff --git a/src/platform_edit.py b/src/platform_edit.py index b625550..a3d14a6 100644 --- a/src/platform_edit.py +++ b/src/platform_edit.py @@ -65,7 +65,7 @@ class PlatformEdit(QWidget): if self.plat.fund_deal_fee is None: self.check_boxes_ticked[1] = False - self.plat_fund_deal_fee_check.setChecked(False) + self.fund_deal_fee_check.setChecked(False) else: self.check_boxes_ticked[1] = True self.fund_deal_fee_check.setChecked(True) @@ -317,8 +317,6 @@ class PlatformEdit(QWidget): prev_box_row = cur_box_pos[0] - 1 prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget() - #if loading: - # prev_box_item.setValue(self.plat.fund_plat_fee[0][x+1]) cur_label_item = self.gridLayout_2.itemAtPosition(cur_label_pos[0], cur_label_pos[1]).widget() cur_label_item.setText(f"between £{int(prev_box_item.value())} and") diff --git a/src/platform_list.py b/src/platform_list.py index 63a3c1f..0630462 100644 --- a/src/platform_list.py +++ b/src/platform_list.py @@ -1,7 +1,7 @@ from PyQt6 import uic from PyQt6.QtCore import QRegularExpression -from PyQt6.QtGui import QIcon, QRegularExpressionValidator -from PyQt6.QtWidgets import QWidget, QListWidgetItem, QDialog +from PyQt6.QtGui import QIcon, QRegularExpressionValidator, QFont +from PyQt6.QtWidgets import QWidget, QListWidgetItem, QDialog, QDialogButtonBox, QMessageBox import resource_finder from db_handler import DBHandler @@ -34,6 +34,25 @@ class PlatformRename(QDialog): event.ignore() self.reject() + +class RemoveConfirm(QMessageBox): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + self.setWindowTitle("Remove platform?") + self.setIcon(QMessageBox.Icon.Warning) + font = QFont() + font.setPointSize(11) + self.setFont(font) + self.setText("Are you sure you want to remove this platform?") + self.setInformativeText("This action is immediate and permanent") + + self.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel) + self.setDefaultButton(QMessageBox.StandardButton.Cancel) + font.setPointSize(10) + self.findChild(QDialogButtonBox).setFont(font) + + class PlatformList(QWidget): def __init__(self, db: DBHandler): super().__init__() @@ -44,20 +63,12 @@ class PlatformList(QWidget): self.db = db self.plat_edit_win = None self.plat_list_dialog = PlatformRename() + self.del_plat_dialog = RemoveConfirm() self.plat_list = [] self.plat_name_list = [] self.new_plat_name = "" self.update_plat_list() - - for i in range(len(self.plat_name_list)): - plat_name = self.plat_name_list[i] - item = QListWidgetItem() - if plat_name is not None: - item.setText(plat_name) - else: - item.setText(f"Unnamed [ID: {i}]") - - self.platListWidget.addItem(item) + self.db_indices = [x for x in range(len(self.plat_name_list))] # Handle events self.add_plat_but.clicked.connect(self.add_platform) @@ -70,6 +81,17 @@ class PlatformList(QWidget): def update_plat_list(self): self.plat_name_list = self.db.retrieve_plat_list() self.plat_list = self.db.retrieve_platforms() + self.platListWidget.clear() + + for i in range(len(self.plat_name_list)): + plat_name = self.plat_name_list[i] + item = QListWidgetItem() + if plat_name is not None: + item.setText(plat_name) + else: + item.setText(f"Unnamed [ID: {i}]") + + self.platListWidget.addItem(item) def add_platform(self): name_dialog_res = self.plat_list_dialog.exec() @@ -97,6 +119,11 @@ class PlatformList(QWidget): else: self.plat_enabled_check.setChecked(False) + if index in self.db_indices: + self.del_plat_but.setEnabled(True) + else: + self.del_plat_but.setEnabled(False) + def edit_platform(self): index = self.platListWidget.currentRow() self.plat_edit_win = PlatformEdit(self.plat_list[index]) @@ -104,6 +131,8 @@ class PlatformList(QWidget): def save_platforms(self): self.db.write_platforms(self.plat_list) + self.update_plat_list() + self.db_indices = [x for x in range(len(self.plat_name_list))] def toggle_platform_state(self): index = self.platListWidget.currentRow() @@ -111,4 +140,8 @@ class PlatformList(QWidget): self.db.toggle_platform_state(index, state) def remove_platform(self): - return None + index = self.platListWidget.currentRow() + del_dialog_res = self.del_plat_dialog.exec() + if del_dialog_res == QMessageBox.StandardButton.Yes: + self.db.remove_platform(index) + self.update_plat_list() From fa74e9e8da52705593f8eaa21981cd57c3536b98 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Sat, 26 Apr 2025 14:17:17 +0100 Subject: [PATCH 10/15] fix critical calculation bug --- src/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main_window.py b/src/main_window.py index e775f48..3d4e4e2 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -87,7 +87,6 @@ class SIPPCompare(QMainWindow): slider_val: int = self.mix_slider.value() fund_trades_num = int(self.fund_trades_combo.currentText()) share_trades_num = int(self.share_trades_combo.currentText()) - funds_value = (slider_val / 100) * value_num shares_value = (1 - (slider_val / 100)) * value_num for platform in self.platform_list_win.plat_list: @@ -100,6 +99,7 @@ class SIPPCompare(QMainWindow): if platform.fund_deal_fee is not None: fund_deal_fees = fund_trades_num * platform.fund_deal_fee + funds_value = (slider_val / 100) * value_num for i in range(1, len(platform.fund_plat_fee[0])): band = platform.fund_plat_fee[0][i] prev_band = platform.fund_plat_fee[0][i - 1] From 593dec96d166e5b3dace2e3cd38b9c85edbb1ff4 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Sat, 26 Apr 2025 16:37:23 +0100 Subject: [PATCH 11/15] add enable/disable platform functionality --- src/db_handler.py | 3 +-- src/main_window.py | 5 ++++- src/platform_list.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/db_handler.py b/src/db_handler.py index 3543e1e..47c7cdf 100644 --- a/src/db_handler.py +++ b/src/db_handler.py @@ -243,8 +243,7 @@ class DBHandler: return user_details_dict def toggle_platform_state(self, index: int, state: bool): - state_data = [state, index] - self.cur.execute("UPDATE tblPlatforms SET IsEnabled = ? WHERE PlatformID = ?", state_data) + self.cur.execute("UPDATE tblPlatforms SET IsEnabled = ? WHERE PlatformID = ?", [state, index]) self.conn.commit() def remove_platform(self, index: int): diff --git a/src/main_window.py b/src/main_window.py index 3d4e4e2..b999c49 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -77,7 +77,7 @@ class SIPPCompare(QMainWindow): else: self.calc_but.setEnabled(False) - # Calculate fees + # Calculate fees for all active platforms def calculate_fees(self): # Set to empty list each time to avoid persistence self.results = [] @@ -90,6 +90,9 @@ class SIPPCompare(QMainWindow): shares_value = (1 - (slider_val / 100)) * value_num for platform in self.platform_list_win.plat_list: + if not platform.enabled: + continue + fund_plat_fees = 0.0 fund_deal_fees = 0.0 share_plat_fees = 0.0 diff --git a/src/platform_list.py b/src/platform_list.py index 0630462..a2261c0 100644 --- a/src/platform_list.py +++ b/src/platform_list.py @@ -137,7 +137,7 @@ class PlatformList(QWidget): def toggle_platform_state(self): index = self.platListWidget.currentRow() state = self.plat_enabled_check.isChecked() - self.db.toggle_platform_state(index, state) + self.plat_list[index].enabled = state def remove_platform(self): index = self.platListWidget.currentRow() From e2ca29891922f790c6227b2a870fd3d7aef9d8a0 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Sat, 26 Apr 2025 16:38:37 +0100 Subject: [PATCH 12/15] make results window active and raise it when calculating --- src/main_window.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main_window.py b/src/main_window.py index b999c49..07075cf 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -137,7 +137,10 @@ class SIPPCompare(QMainWindow): def show_output_win(self): # Refresh the results when new fees are calculated self.output_win.display_output(self.results, 1) + self.output_win.activateWindow() + self.output_win.raise_() self.output_win.show() + QApplication.alert(self.output_win) def show_platform_list(self): self.platform_list_win.show() From 629ea6833df40217ef87e34bbc3823254d83085d Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Sat, 26 Apr 2025 16:39:05 +0100 Subject: [PATCH 13/15] set min and max sizes for all windows --- gui/dialogs/platform_rename.ui | 12 ++++++++++++ gui/output_window.ui | 12 ++++++++++++ gui/platform_edit.ui | 12 ++++++++++++ gui/platform_list.ui | 17 +++++++++++++---- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/gui/dialogs/platform_rename.ui b/gui/dialogs/platform_rename.ui index a19a166..341d53c 100644 --- a/gui/dialogs/platform_rename.ui +++ b/gui/dialogs/platform_rename.ui @@ -10,6 +10,18 @@ 100 + + + 300 + 100 + + + + + 300 + 100 + + Name Platform diff --git a/gui/output_window.ui b/gui/output_window.ui index 9e3fac9..1425a60 100644 --- a/gui/output_window.ui +++ b/gui/output_window.ui @@ -10,6 +10,18 @@ 685 + + + 1330 + 685 + + + + + 1330 + 685 + + Results diff --git a/gui/platform_edit.ui b/gui/platform_edit.ui index df3d028..f7c638f 100644 --- a/gui/platform_edit.ui +++ b/gui/platform_edit.ui @@ -13,6 +13,18 @@ 567 + + + 630 + 567 + + + + + 630 + 567 + + Platform Editor diff --git a/gui/platform_list.ui b/gui/platform_list.ui index e9d1222..128cde3 100644 --- a/gui/platform_list.ui +++ b/gui/platform_list.ui @@ -2,17 +2,26 @@ PlatformList - - Qt::WindowModality::ApplicationModal - 0 0 - 264 + 263 473 + + + 263 + 473 + + + + + 263 + 473 + + Platform List From 42afd128e55adf43d2948cb5f1cb0c07cb511d26 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Sat, 26 Apr 2025 16:39:56 +0100 Subject: [PATCH 14/15] add error handling for file saving --- src/output_window.py | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/output_window.py b/src/output_window.py index 39a20e1..aad49d1 100644 --- a/src/output_window.py +++ b/src/output_window.py @@ -2,13 +2,33 @@ import os from datetime import datetime from PyQt6 import uic -from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QWidget, QFileDialog +from PyQt6.QtGui import QIcon, QFont +from PyQt6.QtWidgets import QWidget, QFileDialog, QMessageBox, QDialogButtonBox import resource_finder from widgets.mpl_widget import MplWidget +class SaveFailure(QMessageBox): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + self.setWindowTitle("Save failure") + + self.setIcon(QMessageBox.Icon.Critical) + font = QFont() + font.setPointSize(11) + self.setFont(font) + self.setText("Failed to save file") + self.setDetailedText( + "This could be due to a permissions issue, or the file being in use by another process" + ) + + self.setStandardButtons(QMessageBox.StandardButton.Ok) + font.setPointSize(10) + self.findChild(QDialogButtonBox).setFont(font) + + class OutputWindow(QWidget): def __init__(self): super().__init__() @@ -17,6 +37,7 @@ class OutputWindow(QWidget): self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) # Define class variables + self.save_err_dialog = SaveFailure() self.canvas = self.graphWidget.canvas self.ax = self.canvas.axes self.fig = self.canvas.figure @@ -39,6 +60,9 @@ class OutputWindow(QWidget): names.append(result[4]) values.append(sum(result[:4]) * years) + names = sorted(names, key=lambda x: values[names.index(x)], reverse=True) + values = sorted(values, reverse=True) + h_bars = self.ax.barh(names, values) self.ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues]) @@ -58,10 +82,11 @@ class OutputWindow(QWidget): try: self.fig.savefig(file_path, dpi=150) - except: - pass + except OSError: + self.save_err_dialog.exec() def save_csv(self): + # TODO: Sort CSV output, either alphabetically or by total fees file_picker = QFileDialog(self) file_picker.setFileMode(QFileDialog.FileMode.AnyFile) file_picker.setDefaultSuffix("csv") @@ -105,7 +130,7 @@ class OutputWindow(QWidget): csvfile.write(csv_string) csvfile.close() except OSError: - print("ERROR FILE SAVE FAILED") + self.save_err_dialog.exec() def change_time(self): years: int = self.time_slider.value() From ba204becc9e6bc2fda1ffa3419e0cf6771c3a552 Mon Sep 17 00:00:00 2001 From: Roland W-H Date: Sat, 26 Apr 2025 16:40:36 +0100 Subject: [PATCH 15/15] code formatting, clean up & comments --- src/main_window.py | 21 ++++++--------------- src/platform_list.py | 1 + src/widgets/fastedit_spinbox.py | 1 + 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/main_window.py b/src/main_window.py index 07075cf..d35fc3e 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,33 +1,21 @@ from PyQt6 import uic from PyQt6.QtGui import QIntValidator, QIcon -from PyQt6.QtWidgets import QMainWindow +from PyQt6.QtWidgets import QMainWindow, QApplication import resource_finder from db_handler import DBHandler -from output_window import OutputWindow from platform_list import PlatformList +from output_window import OutputWindow class SIPPCompare(QMainWindow): def __init__(self): super().__init__() # Import Qt Designer UI XML file - uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) # Initialise class variables - # Inputs - self.optional_boxes = [] - self.fund_plat_fee = 0.0 - self.plat_name = "" - self.fund_deal_fee = 0.0 - self.share_plat_fee = 0.0 - self.share_plat_max_fee = 0.0 - self.share_deal_fee = 0.0 - self.share_deal_reduce_trades = 0.0 - self.share_deal_reduce_amount = 0.0 - # Results self.fund_plat_fees = 0.0 self.fund_deal_fees = 0.0 @@ -42,11 +30,12 @@ class SIPPCompare(QMainWindow): # Handle events self.calc_but.clicked.connect(self.calculate_fees) - # Menu bar entry (File -> Edit Platforms) + # Menu bar entry (File -> Platform List) self.actionList_Platforms.triggered.connect(self.show_platform_list) # Update percentage mix label when slider moved self.mix_slider.valueChanged.connect(self.update_slider_lab) self.value_input.valueChanged.connect(self.check_valid) + # Validate input self.share_trades_combo.currentTextChanged.connect(self.check_valid) self.fund_trades_combo.currentTextChanged.connect(self.check_valid) @@ -69,6 +58,7 @@ class SIPPCompare(QMainWindow): mix_lab_str = f"Investment mix (funds {slider_val}% / shares {100 - slider_val}%)" self.mix_lab.setText(mix_lab_str) + # Ensure that trade fields aren't blank and pension value > 0 def check_valid(self): if self.share_trades_combo.currentText() != "" \ and self.fund_trades_combo.currentText() != "" \ @@ -130,6 +120,7 @@ class SIPPCompare(QMainWindow): self.results.append([fund_plat_fees, fund_deal_fees, share_plat_fees, share_deal_fees, plat_name]) + # Save details entered by user for next session self.db.write_user_details(value_num, slider_val, share_trades_num, fund_trades_num) self.show_output_win() diff --git a/src/platform_list.py b/src/platform_list.py index a2261c0..77574e1 100644 --- a/src/platform_list.py +++ b/src/platform_list.py @@ -60,6 +60,7 @@ class PlatformList(QWidget): uic.loadUi(resource_finder.get_res_path("gui/platform_list.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + # Initialise class variables self.db = db self.plat_edit_win = None self.plat_list_dialog = PlatformRename() diff --git a/src/widgets/fastedit_spinbox.py b/src/widgets/fastedit_spinbox.py index dcdc692..ae93506 100644 --- a/src/widgets/fastedit_spinbox.py +++ b/src/widgets/fastedit_spinbox.py @@ -7,6 +7,7 @@ class FastEditQDoubleSpinBox(QDoubleSpinBox): QTimer.singleShot(0, self.selectAll) super(FastEditQDoubleSpinBox, self).focusInEvent(e) + class FastEditQSpinBox(QSpinBox): def focusInEvent(self, e): QTimer.singleShot(0, self.selectAll)