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

Henry Wu
6 min readDec 26, 2023

--

Previously

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

--

--

Henry Wu
Henry Wu

Written by Henry Wu

Indie Developer/ Business Analyst/ Python/ AI/ Former Journalist/ Codewriter & Copywriter