Compare commits

...

9 Commits

10 changed files with 160 additions and 133 deletions

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

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>420</width>
<height>240</height>
<height>243</height>
</rect>
</property>
<property name="minimumSize">
@ -16,12 +16,6 @@
<height>240</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>420</width>
<height>240</height>
</size>
</property>
<property name="windowTitle">
<string>SIPPCompare</string>
</property>
@ -35,7 +29,7 @@
<x>10</x>
<y>0</y>
<width>401</width>
<height>184</height>
<height>188</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
@ -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>
@ -235,11 +238,16 @@
</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>calc_but</tabstop>
</tabstops>
<resources/>

View File

@ -10,19 +10,19 @@
<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>567</height>
<height>580</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>630</width>
<height>567</height>
<height>580</height>
</size>
</property>
<property name="windowTitle">
@ -31,10 +31,10 @@
<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">
@ -54,10 +54,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/>
</property>
<property name="checked">
<bool>true</bool>
</property>
@ -336,7 +339,7 @@
<property name="geometry">
<rect>
<x>482</x>
<y>534</y>
<y>545</y>
<width>141</width>
<height>24</height>
</rect>
@ -354,7 +357,7 @@
<property name="geometry">
<rect>
<x>8</x>
<y>540</y>
<y>549</y>
<width>191</width>
<height>21</height>
</rect>
@ -366,7 +369,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>
@ -382,16 +385,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">
@ -467,7 +482,7 @@
<property name="geometry">
<rect>
<x>532</x>
<y>481</y>
<y>487</y>
<width>91</width>
<height>24</height>
</rect>
@ -488,7 +503,7 @@
<property name="geometry">
<rect>
<x>440</x>
<y>481</y>
<y>487</y>
<width>91</width>
<height>24</height>
</rect>
@ -505,8 +520,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>
@ -520,11 +535,11 @@
<string>on the value above £ there is no charge</string>
</property>
</widget>
<widget class="QLabel" name="label">
<widget class="QLabel" name="fund_plat_fee_lab">
<property name="geometry">
<rect>
<x>10</x>
<y>284</y>
<y>285</y>
<width>151</width>
<height>16</height>
</rect>
@ -541,13 +556,13 @@
</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

@ -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,10 +243,12 @@ class DBHandler:
return user_details_dict
# Toggle whether fees for this platform should be calculated
def toggle_platform_state(self, index: int, state: bool):
self.cur.execute("UPDATE tblPlatforms SET IsEnabled = ? WHERE PlatformID = ?", [state, index])
self.conn.commit()
# 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:

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,4 +1,5 @@
from PyQt6 import uic
from PyQt6.QtCore import QTimer
from PyQt6.QtGui import QIntValidator, QIcon
from PyQt6.QtWidgets import QMainWindow, QApplication
@ -15,7 +16,7 @@ class SIPPCompare(QMainWindow):
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
## Initialise class variables
# Results
self.fund_plat_fees = 0.0
self.fund_deal_fees = 0.0
@ -26,30 +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)
## 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)
# Validate input
self.share_trades_combo.currentTextChanged.connect(self.check_valid)
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
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)
@ -60,13 +59,15 @@ class SIPPCompare(QMainWindow):
# 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)
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
@ -75,9 +76,10 @@ 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())
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:
@ -88,6 +90,8 @@ class SIPPCompare(QMainWindow):
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
@ -119,6 +123,7 @@ 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)
@ -127,7 +132,9 @@ 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 = 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()

View File

@ -6,7 +6,6 @@ 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):

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
@ -178,6 +178,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 +301,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)
@ -337,7 +339,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)
@ -347,3 +349,7 @@ class PlatformEdit(QWidget):
self.check_valid()
self.update_tier_labels()
def closeEvent(self, event):
event.ignore()
self.reject()

View File

@ -63,7 +63,7 @@ class PlatformList(QWidget):
# 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 = []
@ -95,6 +95,7 @@ class PlatformList(QWidget):
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
@ -110,13 +111,19 @@ 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)
@ -127,8 +134,9 @@ class PlatformList(QWidget):
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)
@ -138,7 +146,8 @@ class PlatformList(QWidget):
def toggle_platform_state(self):
index = self.platListWidget.currentRow()
state = self.plat_enabled_check.isChecked()
self.plat_list[index].enabled = state
if len(self.plat_list) > 0 and index >= 0:
self.plat_list[index].enabled = state
def remove_platform(self):
index = self.platListWidget.currentRow()

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)