diff --git a/gui/platform_edit.ui b/gui/platform_edit.ui index 0cc4a1f..ddf9a0e 100644 --- a/gui/platform_edit.ui +++ b/gui/platform_edit.ui @@ -6,20 +6,20 @@ 0 0 - 480 - 305 + 498 + 303 - 480 - 305 + 498 + 303 - 480 - 305 + 498 + 303 @@ -28,10 +28,10 @@ - 10 - 10 + 20 + 20 461 - 251 + 243 @@ -53,99 +53,166 @@ 5 - - - - Platform name + + + + true + + + + + + + false + + + true + + + + + + + false + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + + + £ + + + 999.000000000000000 + + + + + + + false + + + true + + + + + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + + + £ + + + 999.000000000000000 - - - Share dealing discount amount - - - - - - - - - - Share platform fee* - - - - Share dealing discount # of trades + + + + + + Share platform monthly fee cap + + + + + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + + + £ + + + 999.000000000000000 + + + + Share dealing fee* - + + + + false + + + QAbstractSpinBox::ButtonSymbols::NoButtons + + + QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + + + 999 + + + + + + + + + + false + + + true + + + + Fund dealing fee* - - + + + + + + + + - Share platform fee cap/mth + Share dealing discount amount - - - - QAbstractSpinBox::ButtonSymbols::NoButtons - - - QAbstractSpinBox::CorrectionMode::CorrectToNearestValue - - - £ - - - - - - - QAbstractSpinBox::ButtonSymbols::NoButtons - - - QAbstractSpinBox::CorrectionMode::CorrectToNearestValue - - - £ + + + + Share platform fee* - - - QAbstractSpinBox::ButtonSymbols::NoButtons - - - QAbstractSpinBox::CorrectionMode::CorrectToNearestValue - - - £ - - - - - + QAbstractSpinBox::ButtonSymbols::NoButtons @@ -155,10 +222,23 @@ % + + 99.000000000000000 + - - + + + + Platform name + + + + + + + false + QAbstractSpinBox::ButtonSymbols::NoButtons @@ -168,15 +248,8 @@ £ - - - - - - QAbstractSpinBox::ButtonSymbols::NoButtons - - - QAbstractSpinBox::CorrectionMode::CorrectToNearestValue + + 999.000000000000000 @@ -188,7 +261,7 @@ - 330 + 350 270 141 24 @@ -198,11 +271,11 @@ Save - + - 20 - 270 + 10 + 280 191 21 @@ -211,7 +284,35 @@ * Indicates required field + + + + 437 + 10 + 61 + 16 + + + + Active? + + + Qt::AlignmentFlag::AlignCenter + + + + + FastEditQDoubleSpinBox + QDoubleSpinBox +
widgets/fastedit_spinbox
+
+ + FastEditQSpinBox + QSpinBox +
widgets/fastedit_spinbox
+
+
plat_name_box fund_deal_fee_box @@ -221,6 +322,13 @@ share_deal_reduce_trades_box share_deal_reduce_amount_box save_but + plat_name_check + fund_deal_fee_check + share_plat_fee_check + share_plat_max_fee_check + share_deal_fee_check + share_deal_reduce_trades_check + share_deal_reduce_amount_check diff --git a/src/main_window.py b/src/main_window.py index 14060e3..093e511 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -14,6 +14,7 @@ class SIPPCompare(QMainWindow): # Initialise class variables # Inputs + self.optional_boxes = [] self.fund_plat_fee = 0.0 self.plat_name = "" self.fund_deal_fee = 0.0 @@ -39,7 +40,7 @@ class SIPPCompare(QMainWindow): self.actionEdit_Platforms.triggered.connect(self.show_platform_edit) # 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.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) @@ -55,24 +56,42 @@ class SIPPCompare(QMainWindow): def check_valid(self): if self.share_trades_combo.currentText() != "" \ - and self.fund_trades_combo.currentText() != "": + and self.fund_trades_combo.currentText() != "" \ + and self.value_input.value() != 0: self.calc_but.setEnabled(True) else: self.calc_but.setEnabled(False) # Get variables from platform editor input fields def init_variables(self): - self.plat_name = self.platform_win.get_plat_name() - self.fund_plat_fee = self.platform_win.get_fund_plat_fee() - self.fund_deal_fee = self.platform_win.get_fund_deal_fee() - self.share_plat_fee = self.platform_win.get_share_plat_fee() - self.share_plat_max_fee = self.platform_win.get_share_plat_max_fee() - self.share_deal_fee = self.platform_win.get_share_deal_fee() - self.share_deal_reduce_trades = self.platform_win.get_share_deal_reduce_trades() - self.share_deal_reduce_amount = self.platform_win.get_share_deal_reduce_amount() + self.optional_boxes = self.platform_win.get_optional_boxes() + self.fund_plat_fee = self.platform_win.get_fund_plat_fee() + self.fund_deal_fee = self.platform_win.get_fund_deal_fee() + self.share_plat_fee = self.platform_win.get_share_plat_fee() + self.share_deal_fee = self.platform_win.get_share_deal_fee() + + # TODO: This is HORRIBLE - find better way of doing it! (maybe enums?) + if self.optional_boxes[0]: + self.plat_name = self.platform_win.get_plat_name() + else: + self.plat_name = None + + if self.optional_boxes[1]: + self.share_plat_max_fee = self.platform_win.get_share_plat_max_fee() + else: + self.share_plat_max_fee = None + + if self.optional_boxes[2]: + self.share_deal_reduce_trades = self.platform_win.get_share_deal_reduce_trades() + else: + self.share_deal_reduce_trades = None + + if self.optional_boxes[3]: + self.share_deal_reduce_amount = self.platform_win.get_share_deal_reduce_amount() + else: + self.share_deal_reduce_amount = None # Calculate fees - # TODO: Error checking on combo boxes def calculate_fees(self): self.init_variables() # Set to zero each time to avoid persistence @@ -98,13 +117,15 @@ class SIPPCompare(QMainWindow): break shares_value = (1 - (slider_val / 100)) * value_num - if (self.share_plat_fee * shares_value / 12) > self.share_plat_max_fee: + if self.share_plat_max_fee is not None and \ + (self.share_plat_fee * shares_value / 12) > self.share_plat_max_fee: self.share_plat_fees = self.share_plat_max_fee * 12 else: self.share_plat_fees = self.share_plat_fee * shares_value share_trades_num = int(self.share_trades_combo.currentText()) - if (share_trades_num / 12) >= self.share_deal_reduce_trades: + if self.share_deal_reduce_trades is not None and \ + (share_trades_num / 12) >= self.share_deal_reduce_trades: self.share_deal_fees = self.share_deal_reduce_amount * share_trades_num else: self.share_deal_fees = self.share_deal_fee * share_trades_num diff --git a/src/output_window.py b/src/output_window.py index e9fcfb7..1765a51 100644 --- a/src/output_window.py +++ b/src/output_window.py @@ -23,7 +23,12 @@ class OutputWindow(QWidget): cur_time = datetime.datetime.now() if not os.path.exists("output"): os.makedirs("output") - filename_str = f"output/{self.platform_name}-{cur_time.year}.{cur_time.month}.{cur_time.day}.txt" + filename_str = f"output/" + if self.platform_name is not None: + filename_str += f"{self.platform_name}" + else: + filename_str += "Unnamed" + filename_str += f"-{cur_time.year}.{cur_time.month}.{cur_time.day}.txt" output_file = open(filename_str, "wt", encoding = "utf-8") output_file.write(self.results_str) @@ -31,7 +36,10 @@ class OutputWindow(QWidget): def display_output(self, fund_plat_fees: float, fund_deal_fees: float, share_plat_fees: float, share_deal_fees: float, plat_name: str): self.platform_name = plat_name - self.results_str = f"Fees breakdown (Platform \"{self.platform_name}\"):" + if self.platform_name is not None: + self.results_str = f"Fees breakdown (Platform \"{self.platform_name}\"):" + else: + self.results_str = f"Fees breakdown:" self.results_str += "\n\nPlatform fees:" # :.2f is used in order to display 2 decimal places (currency form) diff --git a/src/platform_edit.py b/src/platform_edit.py index fda14dd..55f4e4a 100644 --- a/src/platform_edit.py +++ b/src/platform_edit.py @@ -1,4 +1,4 @@ -from PyQt6.QtCore import QRegularExpression, QEvent, QObject, QTimer +from PyQt6.QtCore import QRegularExpression from PyQt6.QtGui import QRegularExpressionValidator from PyQt6.QtWidgets import QWidget from PyQt6 import uic @@ -13,6 +13,9 @@ class PlatformEdit(QWidget): uic.loadUi("gui/platform_edit.ui", self) # Initialise class variables + # Create main window object, passing this instance as param + self.main_win = main_window.SIPPCompare(self) + # TODO: Make fund_plat_fee user-defined self.fund_plat_fee = [ [0, 250000, 1000000, 2000000], @@ -30,23 +33,49 @@ class PlatformEdit(QWidget): if autofill: self.save_but.setEnabled(True) - # Create main window object, passing this instance as param - self.main_win = main_window.SIPPCompare(self) + self.required_fields = [ + self.fund_deal_fee_box, + self.share_plat_fee_box, + self.share_deal_fee_box + ] + + self.optional_fields = [ + self.plat_name_box, + self.share_plat_max_fee_box, + self.share_deal_reduce_trades_box, + self.share_deal_reduce_amount_box + ] + + self.optional_check_boxes = [ + self.plat_name_check, + self.share_plat_max_fee_check, + self.share_deal_reduce_trades_check, + self.share_deal_reduce_amount_check + ] + + self.check_boxes_ticked = [ + True, + False, + False, + False + ] # Handle events + for field in self.required_fields: + field.valueChanged.connect(self.check_valid) + + for field in self.optional_fields: + field_type = field.staticMetaObject.className() + if field_type == "QLineEdit": + field.textChanged.connect(self.check_valid) + elif field_type == "FastEditQDoubleSpinBox" or field_type == "FastEditQSpinBox": + field.valueChanged.connect(self.check_valid) + + for check_box in self.optional_check_boxes: + check_box.checkStateChanged.connect(self.check_valid) + # NOTE: Signal defined in UI file to close window when save button clicked self.save_but.clicked.connect(self.init_variables) - self.fund_deal_fee_box.valueChanged.connect(self.check_valid) - self.share_plat_fee_box.valueChanged.connect(self.check_valid) - self.share_deal_fee_box.valueChanged.connect(self.check_valid) - - # Install event filter on input boxes in order to select all text on focus - self.fund_deal_fee_box.installEventFilter(self) - self.share_plat_fee_box.installEventFilter(self) - self.share_plat_max_fee_box.installEventFilter(self) - self.share_deal_fee_box.installEventFilter(self) - self.share_deal_reduce_trades_box.installEventFilter(self) - self.share_deal_reduce_amount_box.installEventFilter(self) # Set validators # Regex accepts any characters that match [a-Z], [0-9] or _ @@ -77,33 +106,54 @@ class PlatformEdit(QWidget): # Once user input is received show main window self.main_win.show() - # When focus is given to an input box, select all text in it (easier to edit) - def eventFilter(self, obj: QObject, event: QEvent): - if event.type() == QEvent.Type.FocusIn: - # Alternative condition for % suffix - currently unused - #if obj.value() == 0 or obj == self.share_plat_fee_box: - QTimer.singleShot(0, obj.selectAll) - return False - - # Check if all required fields have valid (non-zero) input - # TODO: Find a better way of doing this if possible + # 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 + # 3) Check all optional fields the user has picked have a non-zero value + # 4) If the above two conditions are met (1 & 3), make the 'Save' button clickable + # 5) Keep a record of which optional fields the user has chosen to fill in + # It's called when an optional check box emits a checkStateChanged() signal + # It's also called when any field emits a textChanged() or valueChanged() signal def check_valid(self): - values = [self.fund_deal_fee_box.value(), - self.share_plat_fee_box.value(), - self.share_deal_fee_box.value() - ] valid = True - for value in values: - if value == 0: + # Check all required fields have a non-zero value + for field in self.required_fields: + if field.value() == 0: valid = False + for i in range(len(self.optional_check_boxes)): + # Find the coordinates of the input box corresponding to the checkbox + # It will be on the same row, in the column to the left (-1) + check_box_idx = self.gridLayout.indexOf(self.optional_check_boxes[i]) + check_box_pos = self.gridLayout.getItemPosition(check_box_idx) + input_box_pos = list(check_box_pos)[:2] + input_box_pos[1] -= 1 + # Return copy of input field widget from its coordinates + input_box_item = self.gridLayout.itemAtPosition(input_box_pos[0], input_box_pos[1]).widget() + if self.optional_check_boxes[i].isChecked(): + input_box_item.setEnabled(True) + self.check_boxes_ticked[i] = True + input_box_type = input_box_item.staticMetaObject.className() + if input_box_type == "QLineEdit": + if input_box_item.text() == "": + valid = False + elif input_box_type == "FastEditQDoubleSpinBox" or input_box_type == "FastEditQSpinBox": + if input_box_item.value() == 0: + valid = False + else: + input_box_item.setEnabled(False) + self.check_boxes_ticked[i] = False + if valid: self.save_but.setEnabled(True) else: self.save_but.setEnabled(False) # Getter functions (is this necessary? maybe directly reading class vars would be best...) + def get_optional_boxes(self): + return self.check_boxes_ticked + def get_plat_name(self): return self.plat_name diff --git a/src/widgets/fastedit_spinbox.py b/src/widgets/fastedit_spinbox.py new file mode 100644 index 0000000..dcdc692 --- /dev/null +++ b/src/widgets/fastedit_spinbox.py @@ -0,0 +1,13 @@ +from PyQt6.QtCore import QTimer +from PyQt6.QtWidgets import QSpinBox, QDoubleSpinBox + + +class FastEditQDoubleSpinBox(QDoubleSpinBox): + def focusInEvent(self, e): + QTimer.singleShot(0, self.selectAll) + super(FastEditQDoubleSpinBox, self).focusInEvent(e) + +class FastEditQSpinBox(QSpinBox): + def focusInEvent(self, e): + QTimer.singleShot(0, self.selectAll) + super(FastEditQSpinBox, self).focusInEvent(e)