How to Build an Online Database APP by Python, PySide6 and Google Sheets (3): Add Investor | by Henry Wu | Medium

Source: How to Build an Online Database APP by Python, PySide6 and Google Sheets (3): Add Investor | by Henry Wu | Medium

6 min read

Dec 27, 2023

Previously

How to Build an Online Database APP by Python, PySide6 and Google Sheets (2): Main Window

Previously

ihenrywu.medium.com

This article demonstrates how to realize the function of “Add Account.”

Previously

How to Build an Online Database APP by Python, PySide6 and Google Sheets (1): Google Sheets API

Previously

ihenrywu.medium.com

How to Build an Online Database APP by Python, PySide6 and Google Sheets (2): Main Window

Previously

ihenrywu.medium.com

Call the Function in MainWindow Class

According to the last article, we already had code in the MainWindow class:

class MainWindow(QMainWindow):
    def __init__(self, google_sheets_client):
        super().__init__()
        #### 

    def handle_secondary_action(self, top_level_item, second_level_item):
        # Handle the action based on the top-level and second-level item
        if top_level_item == "Investors" and second_level_item == "Add Investor":
            self.display_add_investor_form()
        # ...

handle_secondary_action: Handles actions based on the selected items from the UI. For example, if “Investors” and “Add Investor” are selected, it calls display_add_investor_form.

class MainWindow(QMainWindow):
    def display_add_investor_form(self):
        # Clear existing content
        self.clear_content()

        # Set the main content label's text for Add Investor
        self.update_main_content("")

        # Display the AddInvestorForm
        add_investor_form = AddInvestorForm(google_sheets_client)
        self.content.layout().addWidget(add_investor_form)

Create Add Investor Class

  • __init__: Constructor setting up the form to add a new investor. It includes initializing various input fields and comboboxes with predefined options.
class AddInvestorForm(QWidget):
    def __init__(self, google_sheets_client):
        super().__init__()

        self.google_sheets_client = google_sheets_client
        self.worksheet_name = "01_Investors"

        # Grouped Fields
        self.group1_fields = ["ID_Investor", "Type", "Last Name", "First Name", "Company Name", "Business Number",
                              "Residency Status in Canada", "Date of Birth",  "SIN", "Passport", "Address", "Email", "Phone",
                              "DR", "Accredited Investor", "Excemption Used", "Related Person", "Relationship"]
        self.group2_fields = ["Subscription Agreement Received", "CCO Sign Off", "Trade Confirmation Sent"]
        self.group3_fields = ["Bank Name", "Institution Number", "Branch Number", "Account Number", "Initial Investment",
                              "Money Received On", "Share Issue Date", "Retraction Date", "Share Class",
                              "Share Issued Amount", "Share Certificate Number"]

        self.combobox_fields = ["Type", "Residency Status in Canada", "Accredited Investor", 
                                "Excemption Used", "Subscription Agreement Received", 
                                "Reinvest/Cash Dividen", "Trade Confirmation Sent", 
                                "CCO Sign Off", "DR"]

        # Define the options for each combobox field
        self.combobox_options = {
            "Type": ["Individual", "Entity"],
            "Residency Status in Canada": ["PR", "Citizen", "China(HK)", "China(Mainland)", "US", "Other"],
            "Accredited Investor": ["Yes", "No"],
            "Excemption Used": ["Family Members", "Close Personal Friend", "Close Business Associates"],
            "Subscription Agreement Received": ["Yes", "No"],
            #"Reinvest/Cash Dividend": ["Reinvest", "Cash Dividend"],
            "Trade Confirmation Sent": ["Yes", "No"],
            "CCO Sign Off": ["Yes", "No"],
            "DR": ["DR1", "DR2", "DR3", "DR4"]
        }

        # Labels (assuming they are the same as field names for simplicity)
        self.group1_labels = self.group1_fields
        self.group2_labels = self.group2_fields
        self.group3_labels = self.group3_fields

        # Initialize input_fields as a dictionary of QLineEdit and QComboBox
        self.input_fields = {}
        for field in (self.group1_fields + self.group2_fields + self.group3_fields):
            if field in self.combobox_fields:
                combobox = QComboBox()
                combobox.addItem("")  # Add empty default choice
                combobox.addItems(self.combobox_options[field])
                if field == "Type":
                    combobox.currentIndexChanged.connect(self.handle_type_change)
                self.input_fields[field] = combobox
            else:
                line_edit = QLineEdit()
                if field == "ID_Investor":
                    line_edit.setText(self.generate_new_investor_id())  # Set new ID  
                    line_edit.setReadOnly(True)
                    line_edit.setStyleSheet("background-color: lightgrey;") 
                if field == "Date of Birth":
                    line_edit.setPlaceholderText("YYYY-MM-DD")  # Set the placeholder"            
                self.input_fields[field] = line_edit

        # Now call initUI and pass the submit button text
        self.initUI()
  • initUI: Sets up the UI layout for the form, including scrollable areas and button layouts.
    def initUI(self):
        # Main layout within a scrollable area
        scroll = QScrollArea(self)
        scroll.setWidgetResizable(True)
        content_widget = QWidget()
        scroll.setWidget(content_widget)
        layout = QVBoxLayout(content_widget)

        # Add the groups with increased spacing between them
        layout.addWidget(self.create_group_box("Basic and Personal Information", self.group1_fields, self.group1_labels))
        layout.addSpacing(20)  # Double the space between groups
        layout.addWidget(self.create_group_box("Investment Preferences and Status", self.group2_fields, self.group2_labels))
        layout.addSpacing(20)  # Double the space between groups
        #layout.addWidget(self.create_group_box("Banking and Investment Details", self.group3_fields, self.group3_labels))

        # Set the layout for the main widget
        main_layout = QVBoxLayout(self)
        main_layout.addWidget(scroll)

        # "Add Investor" Button outside the group boxes, aligned at the bottom middle
        self.submit_button = QPushButton("Add Investor")
        self.submit_button.clicked.connect(self.submit_form)
        button_layout = QHBoxLayout()
        button_layout.addStretch(1)  # Add stretch to center the button
        button_layout.addWidget(self.submit_button)
        button_layout.addStretch(1)  # Add stretch to center the button
        
        main_layout.addLayout(button_layout)

        self.setLayout(main_layout)
  • create_group_box: Creates and returns a QGroupBox widget with a grid layout containing labels and input fields.
    def create_group_box(self, title, fields, labels):
        group_box = QGroupBox(title)

        group_box.setAlignment(Qt.AlignCenter)  # Center the title
        # Set bold font for the group box title
        title_font = QFont()
        title_font.setPointSize(QLabel().font().pointSize())  # Set the font size to match label size
        #title_font.setBold(True)
        group_box.setFont(title_font)
        
        layout = QGridLayout()

        for i, field_name in enumerate(fields):
            row, col = divmod(i, 2)  # Calculate row and column index
            
            label = QLabel(labels[i])
            label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
            label.setWordWrap(True)
            layout.addWidget(label, row, col * 2)

            input_widget = self.input_fields[field_name]  # Reuse the created input widget
            
            if isinstance(input_widget, QLineEdit):
                input_widget.setAlignment(Qt.AlignCenter)

            layout.addWidget(input_widget, row, col * 2 + 1)

        layout.setColumnStretch(0, 1)
        layout.setColumnStretch(1, 1)
        layout.setColumnStretch(2, 1)
        layout.setColumnStretch(3, 1)

        group_box.setLayout(layout)
        return group_box
  • handle_type_change: Handles changes in the “Type” combobox, enabling or disabling fields based on the selected type.
def handle_type_change(self):
        # This method is triggered when "Type" combobox value is changed
        type_combobox = self.find_input_field("Type")
        is_individual = type_combobox.currentText() == "Individual"

        company_name_field = self.find_input_field("Company Name")
        business_number_field = self.find_input_field("Business Number")

        # Define the styles for disabled and enabled states
        disabled_style = "QLineEdit { background-color: lightgray; }"  # Example color for disabled state
        enabled_style = "QLineEdit { background-color: white; }"  # Example color for enabled state

        if is_individual:
            if company_name_field and isinstance(company_name_field, QLineEdit):
                company_name_field.clear()  # Clear the text
                company_name_field.setEnabled(False)  # Disable the field
                company_name_field.setStyleSheet(disabled_style)  # Apply disabled style

            if business_number_field and isinstance(business_number_field, QLineEdit):
                business_number_field.clear()  # Clear the text
                business_number_field.setEnabled(False)  # Disable the field
                business_number_field.setStyleSheet(disabled_style)  # Apply disabled style
        else:
            if company_name_field and isinstance(company_name_field, QLineEdit):
                company_name_field.setEnabled(True)  # Enable the field
                company_name_field.setStyleSheet(enabled_style)  # Apply enabled style

            if business_number_field and isinstance(business_number_field, QLineEdit):
                business_number_field.setEnabled(True)  # Enable the field
                business_number_field.setStyleSheet(enabled_style)  # Apply enabled style
  • find_input_field: Returns the widget associated with a given field name.
    def find_input_field(self, field_name):
        # Return the widget associated with the given field name
        return self.input_fields.get(field_name, None)
  • generate_new_investor_id: Generates a new unique investor ID based on existing IDs in the Google Sheets database.
    def generate_new_investor_id(self):
        # Fetch existing IDs from the sheet
        worksheet = self.google_sheets_client.open_spreadsheet(GOOGLESHEET_ID).worksheet("01_Investors")
        all_data = worksheet.get_all_values()
        id_column_index = all_data[0].index("ID_Investor")
        existing_ids = [row[id_column_index] for row in all_data[1:] if row[id_column_index].startswith("I_")]

        # Extract year and number
        current_year = datetime.now().year
        prefix = f"I_{current_year}_"
        numbers = [int(id.split('_')[-1]) for id in existing_ids if id.startswith(prefix)]

        # Generate new ID
        new_number = max(numbers, default=0) + 1
        new_id = prefix + f"{new_number:04d}"  # Zero pad to 4 digits
        return new_id
  • is_valid_date: Validates the format of a date string.
    def is_valid_date(self, date_str):
        try:
            # Parse the date string
            date_obj = datetime.strptime(date_str, "%Y-%m-%d")
            
            # Check the year range
            if 1900 <= date_obj.year <= 2020 and 1 <= date_obj.month <= 12 and 1 <= date_obj.day <= 31:
                return True
        except ValueError:
            pass

        return False
  • submit_form: Collects and validates data from the form, updates the Google Sheets database, and displays success or error messages.
    def submit_form(self):

        # Get the date of birth input
        date_of_birth = self.input_fields["Date of Birth"].text()

        # Validate the date of birth
        if not self.is_valid_date(date_of_birth):
            QMessageBox.warning(self, "Invalid Date", "Please enter a valid date of birth, and in the format YYYY-MM-DD")
            return  # Stop further processing


        # Collect data from input fields
        form_data = {}
        for field_name, widget in self.input_fields.items():
            if isinstance(widget, QLineEdit):
                form_data[field_name] = widget.text()
            elif isinstance(widget, QComboBox):
                form_data[field_name] = widget.currentText()
            else:
                form_data[field_name] = None

        try:
            # Connect to the worksheet
            worksheet = self.google_sheets_client.open_spreadsheet(GOOGLESHEET_ID).worksheet(self.worksheet_name)

            # Find the next available row (first empty row)
            next_row = len(worksheet.get_all_values()) + 1

            # Construct the range string for the next row, starting at column 'A'
            range_string = f"A{next_row}"

            # Convert the form data dictionary to a list of values
            # Ensure that the order of fields matches the column order in the sheet
            header_row = worksheet.row_values(1)
            form_data_values = [form_data.get(field, '') for field in header_row]

            # Update the cells starting from the first column of the next row
            #worksheet.update(range_string, [form_data_values], value_input_option='USER_ENTERED')
            #worksheet.update(range_name=range_string, values=[form_data_values], value_input_option='USER_ENTERED')
            worksheet.update(values=[form_data_values], range_name=range_string, value_input_option='USER_ENTERED')

            QMessageBox.information(self, "Success", "Investor data submitted successfully.")
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to submit investor data: {e}")

        # Clear the input fields after submission
        self.clear_input_fields()
  • clear_input_fields: Clears all input fields in the form after submission.
    def clear_input_fields(self):
        # Iterate over the widgets in self.input_fields and clear them
        for widget in self.input_fields.values():
            if isinstance(widget, QLineEdit):
                widget.clear()
            elif isinstance(widget, QComboBox):
                widget.setCurrentIndex(0)  # Reset combobox to the default (empty) selection

Next Step

How to Build an Online Database APP by Python, PySide6 and Google Sheets (4): Edit Investor

Previously

ihenrywu.medium.com

Leave a Reply

The maximum upload file size: 500 MB. You can upload: image, audio, video, document, spreadsheet, interactive, other. Links to YouTube, Facebook, Twitter and other services inserted in the comment text will be automatically embedded. Drop file here