Compare commits

21 Commits

Author SHA1 Message Date
4b8bdfbeea update PyCharm indexing exclusions 2025-04-28 12:55:14 +01:00
09f8b7bba8 remove unused code 2025-04-28 12:54:07 +01:00
abbcbe41d0 final small fixes to UI files 2025-04-27 20:23:48 +01:00
75e26e229a finalise nuitka build options (for windows) 2025-04-27 16:22:06 +01:00
5eb0c3c4a3 fix mistakenly disabled checkbox + fix win size 2025-04-27 15:31:54 +01:00
78323c2ad8 switch from un-used trade QComboBox to FastEditQSpinBox 2025-04-27 15:29:04 +01:00
2422395759 properly implement resource finding for nuitka 2025-04-27 15:28:10 +01:00
7cb41652b4 switch from pyinstaller to nuitka for distribution 2025-04-27 11:56:19 +01:00
ae01d912e1 additional comments 2025-04-27 11:55:49 +01:00
9a8b0045fa defer import of matplotlib to spped up launch 2025-04-27 11:55:27 +01:00
9e79d986e4 add additional input validation 2025-04-27 11:54:09 +01:00
71d9590205 fix weird spacing bug 2025-04-27 11:51:05 +01:00
3b6b75ee48 updated .gitignore 2025-04-26 20:47:52 +01:00
Roland W-H
ca8f4409c1 Merge pull request #3 from RolandWH/db_testing
implement sqlite database and graphed results
2025-04-26 16:45:44 +01:00
ba204becc9 code formatting, clean up & comments 2025-04-26 16:40:36 +01:00
42afd128e5 add error handling for file saving 2025-04-26 16:39:56 +01:00
629ea6833d set min and max sizes for all windows 2025-04-26 16:39:05 +01:00
e2ca298919 make results window active and raise it when calculating 2025-04-26 16:38:37 +01:00
593dec96d1 add enable/disable platform functionality 2025-04-26 16:37:23 +01:00
fa74e9e8da fix critical calculation bug 2025-04-26 14:17:17 +01:00
16802648bc implement deleting platforms and csv saving 2025-04-26 12:57:49 +01:00
16 changed files with 408 additions and 187 deletions

4
.gitignore vendored
View File

@@ -2,8 +2,8 @@
.idea/workspace.xml
.idea/discord.xml
.idea/encodings.xml
build/
dist/
*build/
*dist/
output/
src/*/__pycache__/
src/__pycache__/

2
.idea/SIPPCompare.iml generated
View File

@@ -3,6 +3,8 @@
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/output" />
</content>
<orderEntry type="jdk" jdkName="Python 3.13 (SIPPCompare)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />

View File

@@ -1,45 +0,0 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['src\\main.py'],
pathex=[],
binaries=[],
datas=[('gui/*.ui', 'gui'), ('gui/dialogs/*.ui', 'gui/dialogs'), ('icon2.ico', '.')],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='SIPPCompare',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon="icon2.ico"
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='SIPPCompare',
)

View File

@@ -10,6 +10,18 @@
<height>100</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>300</width>
<height>100</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>300</width>
<height>100</height>
</size>
</property>
<property name="windowTitle">
<string>Name Platform</string>
</property>

View File

@@ -25,9 +25,6 @@
<property name="windowTitle">
<string>SIPPCompare</string>
</property>
<property name="tabShape">
<enum>QTabWidget::TabShape::Rounded</enum>
</property>
<widget class="QWidget" name="centralwidget">
<widget class="QWidget" name="formLayoutWidget">
<property name="geometry">
@@ -35,7 +32,7 @@
<x>10</x>
<y>0</y>
<width>401</width>
<height>184</height>
<height>188</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
@@ -70,9 +67,6 @@
<pointsize>11</pointsize>
</font>
</property>
<property name="frame">
<bool>true</bool>
</property>
<property name="buttonSymbols">
<enum>QAbstractSpinBox::ButtonSymbols::NoButtons</enum>
</property>
@@ -145,18 +139,6 @@
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="fund_trades_combo">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QPushButton" name="calc_but">
<property name="enabled">
@@ -173,14 +155,35 @@
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="share_trades_combo">
<widget class="FastEditQSpinBox" name="share_trades_box">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="editable">
<bool>true</bool>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="prefix">
<string/>
</property>
<property name="maximum">
<number>999</number>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="FastEditQSpinBox" name="fund_trades_box">
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectionMode::CorrectToNearestValue</enum>
</property>
<property name="prefix">
<string/>
</property>
</widget>
</item>
@@ -188,9 +191,6 @@
</widget>
</widget>
<widget class="QStatusBar" name="statusbar">
<property name="enabled">
<bool>true</bool>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
@@ -235,11 +235,18 @@
</property>
</action>
</widget>
<customwidgets>
<customwidget>
<class>FastEditQSpinBox</class>
<extends>QSpinBox</extends>
<header>widgets/fastedit_spinbox</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>value_input</tabstop>
<tabstop>mix_slider</tabstop>
<tabstop>share_trades_combo</tabstop>
<tabstop>fund_trades_combo</tabstop>
<tabstop>share_trades_box</tabstop>
<tabstop>fund_trades_box</tabstop>
<tabstop>calc_but</tabstop>
</tabstops>
<resources/>

View File

@@ -10,6 +10,18 @@
<height>685</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>1330</width>
<height>685</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>1330</width>
<height>685</height>
</size>
</property>
<property name="windowTitle">
<string>Results</string>
</property>
@@ -111,6 +123,11 @@
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>time_slider</tabstop>
<tabstop>save_csv_but</tabstop>
<tabstop>save_graph_but</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -2,27 +2,36 @@
<ui version="4.0">
<class>PlatformEdit</class>
<widget class="QWidget" name="PlatformEdit">
<property name="windowModality">
<enum>Qt::WindowModality::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>630</width>
<height>567</height>
<height>580</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>630</width>
<height>580</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>630</width>
<height>580</height>
</size>
</property>
<property name="windowTitle">
<string>Platform Editor</string>
</property>
<widget class="QWidget" name="gridLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<x>13</x>
<y>20</y>
<width>611</width>
<height>241</height>
<width>616</width>
<height>242</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
@@ -42,10 +51,13 @@
<number>10</number>
</property>
<property name="verticalSpacing">
<number>5</number>
<number>6</number>
</property>
<item row="0" column="3">
<widget class="QCheckBox" name="plat_name_check">
<property name="text">
<string> </string>
</property>
<property name="checked">
<bool>true</bool>
</property>
@@ -218,7 +230,7 @@
</font>
</property>
<property name="text">
<string>Fund dealing fee*</string>
<string>Fund dealing fee</string>
</property>
</widget>
</item>
@@ -324,7 +336,7 @@
<property name="geometry">
<rect>
<x>482</x>
<y>534</y>
<y>545</y>
<width>141</width>
<height>24</height>
</rect>
@@ -342,7 +354,7 @@
<property name="geometry">
<rect>
<x>8</x>
<y>262</y>
<y>549</y>
<width>191</width>
<height>21</height>
</rect>
@@ -354,7 +366,7 @@
<widget class="QLabel" name="active_lab">
<property name="geometry">
<rect>
<x>577</x>
<x>572</x>
<y>10</y>
<width>61</width>
<height>16</height>
@@ -370,16 +382,28 @@
<widget class="QWidget" name="gridLayoutWidget_2">
<property name="geometry">
<rect>
<x>11</x>
<y>309</y>
<width>611</width>
<height>31</height>
<x>19</x>
<y>307</y>
<width>591</width>
<height>40</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="verticalSpacing">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<property name="spacing">
<number>6</number>
</property>
<item row="0" column="3">
<widget class="FastEditQDoubleSpinBox" name="first_tier_fee_box">
<property name="font">
@@ -455,7 +479,7 @@
<property name="geometry">
<rect>
<x>532</x>
<y>481</y>
<y>487</y>
<width>91</width>
<height>24</height>
</rect>
@@ -476,7 +500,7 @@
<property name="geometry">
<rect>
<x>440</x>
<y>481</y>
<y>487</y>
<width>91</width>
<height>24</height>
</rect>
@@ -493,8 +517,8 @@
<widget class="QLabel" name="val_above_lab">
<property name="geometry">
<rect>
<x>6</x>
<y>479</y>
<x>10</x>
<y>486</y>
<width>421</width>
<height>21</height>
</rect>
@@ -508,16 +532,34 @@
<string>on the value above £ there is no charge</string>
</property>
</widget>
<widget class="QLabel" name="fund_plat_fee_lab">
<property name="geometry">
<rect>
<x>10</x>
<y>285</y>
<width>151</width>
<height>16</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Fund platform fee*</string>
</property>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>FastEditQDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<class>FastEditQSpinBox</class>
<extends>QSpinBox</extends>
<header>widgets/fastedit_spinbox</header>
</customwidget>
<customwidget>
<class>FastEditQSpinBox</class>
<extends>QSpinBox</extends>
<class>FastEditQDoubleSpinBox</class>
<extends>QDoubleSpinBox</extends>
<header>widgets/fastedit_spinbox</header>
</customwidget>
</customwidgets>

View File

@@ -2,17 +2,26 @@
<ui version="4.0">
<class>PlatformList</class>
<widget class="QWidget" name="PlatformList">
<property name="windowModality">
<enum>Qt::WindowModality::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>264</width>
<width>263</width>
<height>473</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>263</width>
<height>473</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>263</width>
<height>473</height>
</size>
</property>
<property name="windowTitle">
<string>Platform List</string>
</property>
@@ -122,6 +131,14 @@
</property>
</widget>
</widget>
<tabstops>
<tabstop>platListWidget</tabstop>
<tabstop>add_plat_but</tabstop>
<tabstop>del_plat_but</tabstop>
<tabstop>edit_plat_but</tabstop>
<tabstop>plist_save_but</tabstop>
<tabstop>plat_enabled_check</tabstop>
</tabstops>
<resources/>
<connections>
<connection>

View File

@@ -80,6 +80,7 @@ class DBHandler:
return plat_name_list
# Write updated platform data to DB when changes are saved
def write_platforms(self, plat_list: list[Platform]):
for i in range(len(plat_list)):
platforms_data = [
@@ -242,7 +243,20 @@ 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)
# Remove a platform from the DB - update the IDs to keep them sequential
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()

View File

@@ -1,3 +1,16 @@
## Nuitka compilation options (Windows only)
# nuitka-project: --mode=onefile
# nuitka-project: --enable-plugin=pyqt6
# nuitka-project: --include-module=widgets.mpl_widget
# nuitka-project: --include-data-files=icon2.ico=icon2.ico
# nuitka-project: --include-data-dir=gui=gui
# nuitka-project: --windows-console-mode=disable
# nuitka-project: --windows-icon-from-ico=icon2.ico
# nuitka-project: --product-name=SIPPCompare
# nuitka-project: --file-description=SIPPCompare
# nuitka-project: --product-version=1
# nuitka-project: --output-dir=build
# nuitka-project: --output-filename=SIPPCompare
import sys
from PyQt6.QtWidgets import QApplication

View File

@@ -1,33 +1,22 @@
from PyQt6 import uic
from PyQt6.QtCore import QTimer
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
## Initialise class variables
# Results
self.fund_plat_fees = 0.0
self.fund_deal_fees = 0.0
@@ -38,29 +27,28 @@ class SIPPCompare(QMainWindow):
# Create window objects
self.db = DBHandler()
self.platform_list_win = PlatformList(self.db)
self.output_win = OutputWindow()
if len(self.platform_list_win.plat_name_list) == 0:
QTimer.singleShot(1, self.platform_list_win.show)
self.output_win = None
# Handle events
self.calc_but.clicked.connect(self.calculate_fees)
# Menu bar entry (File -> Edit Platforms)
## Handle events
self.calc_but.clicked.connect(self.indicate_loading)
# 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)
self.share_trades_combo.currentTextChanged.connect(self.check_valid)
self.fund_trades_combo.currentTextChanged.connect(self.check_valid)
# Validate input
self.share_trades_box.valueChanged.connect(self.check_valid)
self.fund_trades_box.valueChanged.connect(self.check_valid)
# Set validators
self.share_trades_combo.setValidator(QIntValidator(0, 999))
self.fund_trades_combo.setValidator(QIntValidator(0, 99))
# Restore last session
## 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.share_trades_box.setValue(prev_session_data["share_trades"])
self.fund_trades_box.setValue(prev_session_data["fund_trades"])
self.calc_but.setFocus()
# Display slider position as mix between two nums (funds/shares)
@@ -69,15 +57,18 @@ 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() != "" \
and self.value_input.value() != 0:
if self.value_input.value() != 0:
self.calc_but.setEnabled(True)
else:
self.calc_but.setEnabled(False)
# Calculate fees
def indicate_loading(self):
self.calc_but.setText("Working...")
QTimer.singleShot(1, self.calculate_fees)
# Calculate fees for all active platforms
def calculate_fees(self):
# Set to empty list each time to avoid persistence
self.results = []
@@ -85,21 +76,27 @@ class SIPPCompare(QMainWindow):
# 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_value = (slider_val / 100) * value_num
fund_trades_num = int(self.fund_trades_box.value())
share_trades_num = int(self.share_trades_box.value())
shares_value = (1 - (slider_val / 100)) * value_num
index = 0
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
share_deal_fees = 0.0
plat_name = platform.plat_name
if plat_name is None or plat_name == "":
plat_name = f"Unnamed [ID: {index}]"
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]
@@ -126,15 +123,22 @@ class SIPPCompare(QMainWindow):
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])
index += 1
# 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()
# 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 = OutputWindow()
self.output_win.display_output(self.results, 1)
self.calc_but.setText("Calculate")
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()

View File

@@ -2,11 +2,30 @@ 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):
@@ -17,6 +36,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
@@ -24,6 +44,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):
@@ -38,19 +59,77 @@ 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])
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 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")
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:
self.save_err_dialog.exec()
def change_time(self):
years: int = self.time_slider.value()

View File

@@ -1,7 +1,7 @@
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.QtWidgets import QLabel, QDialog
import resource_finder
from db_handler import DBHandler
@@ -9,7 +9,7 @@ from data_struct import Platform
from widgets.fastedit_spinbox import FastEditQDoubleSpinBox
class PlatformEdit(QWidget):
class PlatformEdit(QDialog):
def __init__(self, plat: Platform):
super().__init__()
# Import Qt Designer UI XML file
@@ -18,7 +18,6 @@ class PlatformEdit(QWidget):
# Initialise class variables
self.plat = plat
self.fund_plat_fee = self.plat.fund_plat_fee
self.widgets_list_list = []
if len(self.plat.fund_plat_fee[0]) > 1:
self.fund_fee_rows = len(self.plat.fund_plat_fee[0]) - 1
@@ -65,7 +64,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)
@@ -178,6 +177,8 @@ class PlatformEdit(QWidget):
else:
self.plat.share_deal_reduce_amount = None
self.accept()
# This method does multiple things in order to validate the user's inputs:
# 1) Check all required fields have a non-zero value
# 2) If an optional checkbox is toggled: toggle editing of the corresponding field
@@ -299,7 +300,7 @@ class PlatformEdit(QWidget):
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))
self.gridLayoutWidget_2.setGeometry(QRect(19, 307, 591, grid_height))
for i in range(len(widgets)):
if loading:
self.gridLayout_2.addWidget(widgets[i], x + 1, i, 1, 1)
@@ -317,8 +318,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")
@@ -339,7 +338,7 @@ class PlatformEdit(QWidget):
widget.hide()
self.widgets_list_list.pop()
self.fund_fee_rows -= 1
self.gridLayoutWidget_2.setGeometry(11, 309, 611, int(round(28.5 * self.fund_fee_rows, 0)))
self.gridLayoutWidget_2.setGeometry(19, 307, 591, int(round(28.5 * self.fund_fee_rows, 0)))
if self.fund_fee_rows < 2:
self.del_row_but.setEnabled(False)
@@ -349,3 +348,7 @@ class PlatformEdit(QWidget):
self.check_valid()
self.update_tier_labels()
def closeEvent(self, event):
event.ignore()
self.reject()

View File

@@ -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__()
@@ -41,23 +60,16 @@ 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()
self.plat_list_dialog = None
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,8 +82,20 @@ 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):
self.plat_list_dialog = PlatformRename()
name_dialog_res = self.plat_list_dialog.exec()
if name_dialog_res == QDialog.DialogCode.Accepted:
name = self.plat_list_dialog.new_name
@@ -87,28 +111,47 @@ class PlatformList(QWidget):
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()
plat_edit_res = self.plat_edit_win.exec()
if plat_edit_res == QDialog.DialogCode.Rejected:
self.plat_list.pop()
self.platListWidget.takeItem(self.platListWidget.count() - 1)
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)
if len(self.plat_list) > 0:
is_enabled = self.plat_list[index].enabled
if is_enabled:
self.plat_enabled_check.setChecked(True)
else:
self.plat_enabled_check.setChecked(False)
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])
self.plat_edit_win.show()
if len(self.plat_list) > 0:
self.plat_edit_win = PlatformEdit(self.plat_list[index])
self.plat_edit_win.exec()
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()
state = self.plat_enabled_check.isChecked()
self.db.toggle_platform_state(index, state)
if len(self.plat_list) > 0 and index >= 0:
self.plat_list[index].enabled = 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()

View File

@@ -2,12 +2,24 @@ import os.path
import sys
# If using PyInstaller, use it's temporary path, otherwise use cwd
# Credit: https://stackoverflow.com/questions/7674790/bundling-data-files-with-pyinstaller-onefile/13790741#13790741
# Returns the correct path for Nuitka onefile mode, standalone mode or normal Python
# Credit: https://nuitka.net/user-documentation/common-issue-solutions.html#onefile-finding-files
def get_res_path(relative_path):
try:
base_path = sys._MEIPASS
except AttributeError:
base_path = os.path.abspath(".")
path_a = ""
return os.path.join(base_path, relative_path)
try:
path_a = os.path.join(sys.__compiled__.containing_dir, relative_path)
except AttributeError:
pass
path_b = os.path.join(os.path.dirname(__file__), relative_path)
path_c = os.path.join(os.path.dirname(sys.argv[0]), relative_path)
if os.path.isfile(path_a):
return path_a
elif os.path.isfile(path_b):
return path_b
elif os.path.isfile(path_c):
return path_c
else:
return os.path.join(os.path.abspath("."), relative_path)

View File

@@ -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)