Compare commits

23 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
c151b19a3c update pyinstaller spec 2025-04-25 17:36:13 +01:00
f2a0735972 add time slider and saving functionality to output 2025-04-25 17:35:54 +01:00
17 changed files with 519 additions and 192 deletions

4
.gitignore vendored
View File

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

2
.idea/SIPPCompare.iml generated
View File

@@ -3,6 +3,8 @@
<component name="NewModuleRootManager"> <component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" /> <excludeFolder url="file://$MODULE_DIR$/.venv" />
<excludeFolder url="file://$MODULE_DIR$/build" />
<excludeFolder url="file://$MODULE_DIR$/output" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.13 (SIPPCompare)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.13 (SIPPCompare)" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <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'), ('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> <height>100</height>
</rect> </rect>
</property> </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"> <property name="windowTitle">
<string>Name Platform</string> <string>Name Platform</string>
</property> </property>

View File

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

View File

@@ -7,9 +7,21 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>1330</width> <width>1330</width>
<height>630</height> <height>685</height>
</rect> </rect>
</property> </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"> <property name="windowTitle">
<string>Results</string> <string>Results</string>
</property> </property>
@@ -23,6 +35,85 @@
</rect> </rect>
</property> </property>
</widget> </widget>
<widget class="QPushButton" name="save_graph_but">
<property name="geometry">
<rect>
<x>1231</x>
<y>642</y>
<width>91</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Save graph</string>
</property>
</widget>
<widget class="QSlider" name="time_slider">
<property name="geometry">
<rect>
<x>370</x>
<y>635</y>
<width>581</width>
<height>41</height>
</rect>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>20</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TickPosition::TicksAbove</enum>
</property>
<property name="tickInterval">
<number>1</number>
</property>
</widget>
<widget class="QPushButton" name="save_csv_but">
<property name="geometry">
<rect>
<x>1134</x>
<y>642</y>
<width>91</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
</font>
</property>
<property name="text">
<string>Save CSV</string>
</property>
</widget>
<widget class="QLabel" name="time_lab">
<property name="geometry">
<rect>
<x>10</x>
<y>640</y>
<width>341</width>
<height>31</height>
</rect>
</property>
<property name="font">
<font>
<pointsize>11</pointsize>
</font>
</property>
<property name="text">
<string>Fees over 1 year(s) (assuming no change in value)</string>
</property>
</widget>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>
@@ -32,6 +123,11 @@
<container>1</container> <container>1</container>
</customwidget> </customwidget>
</customwidgets> </customwidgets>
<tabstops>
<tabstop>time_slider</tabstop>
<tabstop>save_csv_but</tabstop>
<tabstop>save_graph_but</tabstop>
</tabstops>
<resources/> <resources/>
<connections/> <connections/>
</ui> </ui>

View File

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

View File

@@ -2,17 +2,26 @@
<ui version="4.0"> <ui version="4.0">
<class>PlatformList</class> <class>PlatformList</class>
<widget class="QWidget" name="PlatformList"> <widget class="QWidget" name="PlatformList">
<property name="windowModality">
<enum>Qt::WindowModality::ApplicationModal</enum>
</property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>264</width> <width>263</width>
<height>473</height> <height>473</height>
</rect> </rect>
</property> </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"> <property name="windowTitle">
<string>Platform List</string> <string>Platform List</string>
</property> </property>
@@ -122,6 +131,14 @@
</property> </property>
</widget> </widget>
</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/> <resources/>
<connections> <connections>
<connection> <connection>

View File

@@ -80,6 +80,7 @@ class DBHandler:
return plat_name_list return plat_name_list
# Write updated platform data to DB when changes are saved
def write_platforms(self, plat_list: list[Platform]): def write_platforms(self, plat_list: list[Platform]):
for i in range(len(plat_list)): for i in range(len(plat_list)):
platforms_data = [ platforms_data = [
@@ -242,7 +243,20 @@ class DBHandler:
return user_details_dict return user_details_dict
def toggle_platform_state(self, index: int, state: bool): # Remove a platform from the DB - update the IDs to keep them sequential
state_data = [state, index] def remove_platform(self, index: int):
self.cur.execute("UPDATE tblPlatforms SET IsEnabled = ? WHERE PlatformID = ?", state_data) 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() 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 import sys
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication

View File

@@ -1,33 +1,22 @@
from PyQt6 import uic from PyQt6 import uic
from PyQt6.QtCore import QTimer
from PyQt6.QtGui import QIntValidator, QIcon from PyQt6.QtGui import QIntValidator, QIcon
from PyQt6.QtWidgets import QMainWindow from PyQt6.QtWidgets import QMainWindow, QApplication
import resource_finder import resource_finder
from db_handler import DBHandler from db_handler import DBHandler
from output_window import OutputWindow
from platform_list import PlatformList from platform_list import PlatformList
from output_window import OutputWindow
class SIPPCompare(QMainWindow): class SIPPCompare(QMainWindow):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
# Import Qt Designer UI XML file # Import Qt Designer UI XML file
uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self) uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
# Initialise class variables ## 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 # Results
self.fund_plat_fees = 0.0 self.fund_plat_fees = 0.0
self.fund_deal_fees = 0.0 self.fund_deal_fees = 0.0
@@ -38,29 +27,28 @@ class SIPPCompare(QMainWindow):
# Create window objects # Create window objects
self.db = DBHandler() self.db = DBHandler()
self.platform_list_win = PlatformList(self.db) 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 ## Handle events
self.calc_but.clicked.connect(self.calculate_fees) self.calc_but.clicked.connect(self.indicate_loading)
# Menu bar entry (File -> Edit Platforms) # Menu bar entry (File -> Platform List)
self.actionList_Platforms.triggered.connect(self.show_platform_list) self.actionList_Platforms.triggered.connect(self.show_platform_list)
# Update percentage mix label when slider moved # Update percentage mix label when slider moved
self.mix_slider.valueChanged.connect(self.update_slider_lab) self.mix_slider.valueChanged.connect(self.update_slider_lab)
self.value_input.valueChanged.connect(self.check_valid) self.value_input.valueChanged.connect(self.check_valid)
self.share_trades_combo.currentTextChanged.connect(self.check_valid) # Validate input
self.fund_trades_combo.currentTextChanged.connect(self.check_valid) self.share_trades_box.valueChanged.connect(self.check_valid)
self.fund_trades_box.valueChanged.connect(self.check_valid)
# Set validators ## Restore last session
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() prev_session_data = self.db.retrieve_user_details()
if "NO_RECORD" not in prev_session_data: if "NO_RECORD" not in prev_session_data:
self.value_input.setValue(prev_session_data["pension_val"]) self.value_input.setValue(prev_session_data["pension_val"])
self.mix_slider.setValue(prev_session_data["slider_val"]) self.mix_slider.setValue(prev_session_data["slider_val"])
self.share_trades_combo.setCurrentText(str(prev_session_data["share_trades"])) self.share_trades_box.setValue(prev_session_data["share_trades"])
self.fund_trades_combo.setCurrentText(str(prev_session_data["fund_trades"])) self.fund_trades_box.setValue(prev_session_data["fund_trades"])
self.calc_but.setFocus() self.calc_but.setFocus()
# Display slider position as mix between two nums (funds/shares) # 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}%)" mix_lab_str = f"Investment mix (funds {slider_val}% / shares {100 - slider_val}%)"
self.mix_lab.setText(mix_lab_str) self.mix_lab.setText(mix_lab_str)
# Ensure that trade fields aren't blank and pension value > 0
def check_valid(self): def check_valid(self):
if self.share_trades_combo.currentText() != "" \ if self.value_input.value() != 0:
and self.fund_trades_combo.currentText() != "" \
and self.value_input.value() != 0:
self.calc_but.setEnabled(True) self.calc_but.setEnabled(True)
else: else:
self.calc_but.setEnabled(False) 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): def calculate_fees(self):
# Set to empty list each time to avoid persistence # Set to empty list each time to avoid persistence
self.results = [] self.results = []
@@ -85,21 +76,27 @@ class SIPPCompare(QMainWindow):
# Get user input # Get user input
value_num = float(self.value_input.value()) value_num = float(self.value_input.value())
slider_val: int = self.mix_slider.value() slider_val: int = self.mix_slider.value()
fund_trades_num = int(self.fund_trades_combo.currentText()) fund_trades_num = int(self.fund_trades_box.value())
share_trades_num = int(self.share_trades_combo.currentText()) share_trades_num = int(self.share_trades_box.value())
funds_value = (slider_val / 100) * value_num
shares_value = (1 - (slider_val / 100)) * value_num shares_value = (1 - (slider_val / 100)) * value_num
index = 0
for platform in self.platform_list_win.plat_list: for platform in self.platform_list_win.plat_list:
if not platform.enabled:
continue
fund_plat_fees = 0.0 fund_plat_fees = 0.0
fund_deal_fees = 0.0 fund_deal_fees = 0.0
share_plat_fees = 0.0 share_plat_fees = 0.0
share_deal_fees = 0.0 share_deal_fees = 0.0
plat_name = platform.plat_name 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: if platform.fund_deal_fee is not None:
fund_deal_fees = fund_trades_num * platform.fund_deal_fee 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])): for i in range(1, len(platform.fund_plat_fee[0])):
band = platform.fund_plat_fee[0][i] band = platform.fund_plat_fee[0][i]
prev_band = platform.fund_plat_fee[0][i - 1] 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 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.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.db.write_user_details(value_num, slider_val, share_trades_num, fund_trades_num)
self.show_output_win() self.show_output_win()
# Show the output window - this func is called from calculate_fee() # Show the output window - this func is called from calculate_fee()
def show_output_win(self): def show_output_win(self):
# Refresh the results when new fees are calculated # Refresh the results when new fees are calculated
self.output_win.display_output(self.results) 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() self.output_win.show()
QApplication.alert(self.output_win)
def show_platform_list(self): def show_platform_list(self):
self.platform_list_win.show() self.platform_list_win.show()

View File

@@ -1,13 +1,33 @@
import datetime
import os import os
from datetime import datetime
from PyQt6 import uic from PyQt6 import uic
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon, QFont
from PyQt6.QtWidgets import QWidget from PyQt6.QtWidgets import QWidget, QFileDialog, QMessageBox, QDialogButtonBox
import resource_finder import resource_finder
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): class OutputWindow(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@@ -15,17 +35,103 @@ class OutputWindow(QWidget):
uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self) uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
def display_output(self, results: list): # Define class variables
ax = self.graphWidget.canvas.axes self.save_err_dialog = SaveFailure()
ax.clear() self.canvas = self.graphWidget.canvas
ax.cla() self.ax = self.canvas.axes
self.graphWidget.canvas.draw_idle() self.fig = self.canvas.figure
self.results = []
# 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):
self.results = results
self.ax.clear()
self.ax.cla()
self.canvas.draw_idle()
names = [] names = []
values = [] values = []
for result in results: for result in results:
names.append(result[4]) names.append(result[4])
values.append(sum(result[:4])) values.append(sum(result[:4]) * years)
h_bars = ax.barh(names, values) names = sorted(names, key=lambda x: values[names.index(x)], reverse=True)
ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues]) 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.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"{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()
self.time_lab.setText(f"Fees over {years} year(s) (assuming no change in value)")
self.display_output(self.results, years)

View File

@@ -1,7 +1,7 @@
from PyQt6 import uic from PyQt6 import uic
from PyQt6.QtCore import QRegularExpression, QRect from PyQt6.QtCore import QRegularExpression, QRect
from PyQt6.QtGui import QRegularExpressionValidator, QFont, QIcon from PyQt6.QtGui import QRegularExpressionValidator, QFont, QIcon
from PyQt6.QtWidgets import QWidget, QLabel from PyQt6.QtWidgets import QLabel, QDialog
import resource_finder import resource_finder
from db_handler import DBHandler from db_handler import DBHandler
@@ -9,7 +9,7 @@ from data_struct import Platform
from widgets.fastedit_spinbox import FastEditQDoubleSpinBox from widgets.fastedit_spinbox import FastEditQDoubleSpinBox
class PlatformEdit(QWidget): class PlatformEdit(QDialog):
def __init__(self, plat: Platform): def __init__(self, plat: Platform):
super().__init__() super().__init__()
# Import Qt Designer UI XML file # Import Qt Designer UI XML file
@@ -18,7 +18,6 @@ class PlatformEdit(QWidget):
# Initialise class variables # Initialise class variables
self.plat = plat self.plat = plat
self.fund_plat_fee = self.plat.fund_plat_fee
self.widgets_list_list = [] self.widgets_list_list = []
if len(self.plat.fund_plat_fee[0]) > 1: if len(self.plat.fund_plat_fee[0]) > 1:
self.fund_fee_rows = 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: if self.plat.fund_deal_fee is None:
self.check_boxes_ticked[1] = False self.check_boxes_ticked[1] = False
self.plat_fund_deal_fee_check.setChecked(False) self.fund_deal_fee_check.setChecked(False)
else: else:
self.check_boxes_ticked[1] = True self.check_boxes_ticked[1] = True
self.fund_deal_fee_check.setChecked(True) self.fund_deal_fee_check.setChecked(True)
@@ -178,6 +177,8 @@ class PlatformEdit(QWidget):
else: else:
self.plat.share_deal_reduce_amount = None self.plat.share_deal_reduce_amount = None
self.accept()
# This method does multiple things in order to validate the user's inputs: # This method does multiple things in order to validate the user's inputs:
# 1) Check all required fields have a non-zero value # 1) Check all required fields have a non-zero value
# 2) If an optional checkbox is toggled: toggle editing of the corresponding field # 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)) grid_height = int(round(28.5 * self.fund_fee_rows))
else: else:
grid_height = int(round(28.5 * (self.fund_fee_rows + 1))) 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)): for i in range(len(widgets)):
if loading: if loading:
self.gridLayout_2.addWidget(widgets[i], x + 1, i, 1, 1) 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_row = cur_box_pos[0] - 1
prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget() 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 = 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") cur_label_item.setText(f"between £{int(prev_box_item.value())} and")
@@ -339,7 +338,7 @@ class PlatformEdit(QWidget):
widget.hide() widget.hide()
self.widgets_list_list.pop() self.widgets_list_list.pop()
self.fund_fee_rows -= 1 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: if self.fund_fee_rows < 2:
self.del_row_but.setEnabled(False) self.del_row_but.setEnabled(False)
@@ -349,3 +348,7 @@ class PlatformEdit(QWidget):
self.check_valid() self.check_valid()
self.update_tier_labels() self.update_tier_labels()
def closeEvent(self, event):
event.ignore()
self.reject()

View File

@@ -1,7 +1,7 @@
from PyQt6 import uic from PyQt6 import uic
from PyQt6.QtCore import QRegularExpression from PyQt6.QtCore import QRegularExpression
from PyQt6.QtGui import QIcon, QRegularExpressionValidator from PyQt6.QtGui import QIcon, QRegularExpressionValidator, QFont
from PyQt6.QtWidgets import QWidget, QListWidgetItem, QDialog from PyQt6.QtWidgets import QWidget, QListWidgetItem, QDialog, QDialogButtonBox, QMessageBox
import resource_finder import resource_finder
from db_handler import DBHandler from db_handler import DBHandler
@@ -34,6 +34,25 @@ class PlatformRename(QDialog):
event.ignore() event.ignore()
self.reject() 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): class PlatformList(QWidget):
def __init__(self, db: DBHandler): def __init__(self, db: DBHandler):
super().__init__() super().__init__()
@@ -41,23 +60,16 @@ class PlatformList(QWidget):
uic.loadUi(resource_finder.get_res_path("gui/platform_list.ui"), self) uic.loadUi(resource_finder.get_res_path("gui/platform_list.ui"), self)
self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico")))
# Initialise class variables
self.db = db self.db = db
self.plat_edit_win = None 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_list = []
self.plat_name_list = [] self.plat_name_list = []
self.new_plat_name = "" self.new_plat_name = ""
self.update_plat_list() self.update_plat_list()
self.db_indices = [x for x in range(len(self.plat_name_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)
# Handle events # Handle events
self.add_plat_but.clicked.connect(self.add_platform) self.add_plat_but.clicked.connect(self.add_platform)
@@ -70,8 +82,20 @@ class PlatformList(QWidget):
def update_plat_list(self): def update_plat_list(self):
self.plat_name_list = self.db.retrieve_plat_list() self.plat_name_list = self.db.retrieve_plat_list()
self.plat_list = self.db.retrieve_platforms() 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): def add_platform(self):
self.plat_list_dialog = PlatformRename()
name_dialog_res = self.plat_list_dialog.exec() name_dialog_res = self.plat_list_dialog.exec()
if name_dialog_res == QDialog.DialogCode.Accepted: if name_dialog_res == QDialog.DialogCode.Accepted:
name = self.plat_list_dialog.new_name 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) 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 = 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): def get_enabled_state(self):
index = self.platListWidget.currentRow() index = self.platListWidget.currentRow()
is_enabled = self.plat_list[index].enabled if len(self.plat_list) > 0:
if is_enabled: is_enabled = self.plat_list[index].enabled
self.plat_enabled_check.setChecked(True) if is_enabled:
self.plat_enabled_check.setChecked(True)
else:
self.plat_enabled_check.setChecked(False)
else: else:
self.plat_enabled_check.setChecked(False) 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): def edit_platform(self):
index = self.platListWidget.currentRow() index = self.platListWidget.currentRow()
self.plat_edit_win = PlatformEdit(self.plat_list[index]) if len(self.plat_list) > 0:
self.plat_edit_win.show() self.plat_edit_win = PlatformEdit(self.plat_list[index])
self.plat_edit_win.exec()
def save_platforms(self): def save_platforms(self):
self.db.write_platforms(self.plat_list) 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): def toggle_platform_state(self):
index = self.platListWidget.currentRow() index = self.platListWidget.currentRow()
state = self.plat_enabled_check.isChecked() 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): 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 import sys
# If using PyInstaller, use it's temporary path, otherwise use cwd # Returns the correct path for Nuitka onefile mode, standalone mode or normal Python
# Credit: https://stackoverflow.com/questions/7674790/bundling-data-files-with-pyinstaller-onefile/13790741#13790741 # Credit: https://nuitka.net/user-documentation/common-issue-solutions.html#onefile-finding-files
def get_res_path(relative_path): def get_res_path(relative_path):
try: path_a = ""
base_path = sys._MEIPASS
except AttributeError:
base_path = os.path.abspath(".")
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) QTimer.singleShot(0, self.selectAll)
super(FastEditQDoubleSpinBox, self).focusInEvent(e) super(FastEditQDoubleSpinBox, self).focusInEvent(e)
class FastEditQSpinBox(QSpinBox): class FastEditQSpinBox(QSpinBox):
def focusInEvent(self, e): def focusInEvent(self, e):
QTimer.singleShot(0, self.selectAll) QTimer.singleShot(0, self.selectAll)

View File

@@ -6,8 +6,8 @@ from PyQt6.QtWidgets import QWidget, QVBoxLayout
class MplWidget(QWidget): class MplWidget(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) 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 = QVBoxLayout()
vertical_layout.addWidget(self.canvas) vertical_layout.addWidget(self.canvas)
self.canvas.axes = self.canvas.figure.add_subplot(1, 1, 1) self.canvas.axes = self.canvas.figure.add_subplot(1, 1, 1)
self.setLayout(vertical_layout) self.setLayout(vertical_layout)