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

Henry Wu
6 min readDec 26, 2023

--

Previously

This article is about how to “Add Transaction” in the app:

Call function in the Main Window

  1. display_transaction_form (in MainWindow Class):
  • Clears existing content.
  • Opens SelectAccountDialog for the user to select an account.
  • Fetches account data for the selected account and displays a transaction form using showTransactionForm.
class MainWindow(QMainWindow):
def display_transaction_form(self):
# Clear existing content
self.clear_content()

dialog = SelectAccountDialog(self.google_sheets_client)
result = dialog.exec()

if result == QDialog.Accepted:
selected_id = dialog.getSelectedAccountID()
if selected_id:
account_data = self.fetch_account_data(selected_id)
if account_data:
self.showTransactionForm(account_data)

2. fetch_account_data:

  • Connects to the Google Sheets worksheet named “02_Accounts”.
  • Retrieves and returns account data based on the selected account ID.
    def fetch_account_data(self, account_id):
# Connect to Google Sheets and select the appropriate worksheet
worksheet = self.google_sheets_client.open_spreadsheet(GOOGLESHEET_ID).worksheet("02_Accounts")

# Get the header row (first row) to map column names to their indices
header_row = worksheet.row_values(1)
column_index_map = {name: index for index, name in enumerate(header_row)}

# Ensure all required columns are present
required_columns = ["ID_Account", "ID_Investor", "Type", "Last Name", "First Name", "Company Name", 'Reinvest/Cash Dividend']
for column in required_columns:
if column not in column_index_map:
print(f"Column '{column}' not found in the worksheet.")
return None

# Get all values in the 'Account ID' column
account_id_column = worksheet.col_values(column_index_map["ID_Account"] + 1) # +1 for 1-based index

# Find the row index for the matching account_id
try:
row_index = account_id_column.index(account_id) + 1 # +1 because spreadsheet indices start at 1
except ValueError:
# Account ID not found
return None

# Fetch only the row with the matching Account ID
row_data = worksheet.row_values(row_index)

# Construct account data ensuring all keys exist in column_index_map and row_data
account_data = {}
for key in required_columns:
if key in column_index_map and column_index_map[key] < len(row_data):
account_data[key] = row_data[column_index_map[key]]
else:
account_data[key] = "N/A" # or any default value you prefer

return account_data

3. showTransactionForm:

  • Creates an instance of TransactionForm with the fetched account data.
  • Adds the transaction form widget to the main content layout.
    def showTransactionForm(self, account_data):
# Create the TransactionForm with the fetched account data
print("account_data", account_data)
transaction_form = TransactionForm(self.google_sheets_client, account_data)
self.content.layout().addWidget(transaction_form)

Create Add Transaction Class

  • __init__: Initializes the form, extracts and segregates investor and account data, and sets up input fields for transaction details.
class TransactionForm(QWidget):
def __init__(self, google_sheets_client, account_data):
super().__init__()

self.google_sheets_client = google_sheets_client
self.worksheet_name = "03_Transactions"

# Extract investor and account data
self.display_investor_data = {key: value for key, value in account_data.items() if key in ["ID_Investor", "Type", "Last Name", "First Name", "Company Name"]}
self.display_account_data = {key: value for key, value in account_data.items() if key not in self.display_investor_data}

self.investor_labels = list(self.display_investor_data.keys())
self.investor_fields = list(self.display_investor_data.values())

self.account_labels = list(self.display_account_data.keys())
self.account_fields = list(self.display_account_data.values())


self.transaction_labels = ["Transaction Date:", "Dividend Period:", "Money In (+):", "Money Out (-):"]
self.transaction_fields = ["Transaction Date", "Dividend Period", "Money In", "Money Out"]

# Initialize input fields
self.input_fields = {field: QLineEdit() for field in self.transaction_fields}

# self.input_fields["Transaction Date"].setPlaceholderText("YYYY-MM-DD") # Set the placeholder

# Set the transaction date to the current date, and make it editable
current_date = datetime.now().strftime("%Y-%m-%d")
self.input_fields["Transaction Date"].setText(current_date)
self.input_fields["Transaction Date"].textChanged.connect(self.updateDividendPeriod)

# Initialize and set the Dividend Period field as uneditable
self.updateDividendPeriod(current_date)
self.input_fields["Dividend Period"].setReadOnly(True)
self.input_fields["Dividend Period"].setStyleSheet("background-color: lightgrey;")

self.input_fields["Money In"].setPlaceholderText("$")
self.input_fields["Money Out"].setPlaceholderText("$")

# Initialize UI
self.initUI()
  • initUI: Sets up the UI layout with scrollable areas and group boxes for investor information, account information, and transaction details.
    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 group boxes without additional stretching between them
layout.addWidget(self.create_group_box("Investor Information", self.investor_labels, self.investor_fields, is_editable=False))
layout.addSpacing(20)
layout.addWidget(self.create_group_box("Account Information", self.account_labels, self.account_fields, is_editable=False))
layout.addSpacing(20)
layout.addWidget(self.create_group_box("Transaction Details", self.transaction_labels, self.transaction_fields, is_editable=True))


# Main layout
main_layout = QVBoxLayout(self)
main_layout.addWidget(scroll)

# Submit Button outside the scroll area, centered at the bottom
self.submit_button = QPushButton("Submit")
self.submit_button.clicked.connect(self.submit_transaction)
button_layout = QHBoxLayout()
button_layout.addStretch(1)
button_layout.addWidget(self.submit_button)
button_layout.addStretch(1)

main_layout.addLayout(button_layout)
self.setLayout(main_layout)
  • create_group_box: Creates a QGroupBox for different sections of the form with labels and input fields.
    def create_group_box(self, title, labels, fields, is_editable=True):
group_box = QGroupBox(title)

group_box.setAlignment(Qt.AlignCenter) # Center the title
title_font = QFont()
title_font.setPointSize(QLabel().font().pointSize()) # Set the font size to match label size
group_box.setFont(title_font)

layout = QGridLayout(group_box)

for i, field_name in enumerate(fields):
row, col = divmod(i, 2)

label = QLabel(labels[i])
label.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
label.setWordWrap(True)
layout.addWidget(label, row, col * 2)

# Determine the widget based on the editability
if is_editable:
widget = self.input_fields.get(field_name, QLineEdit())
else:
widget = QLineEdit(str(field_name))
widget.setReadOnly(True)
widget.setStyleSheet("background-color: lightgrey;")

# Align text in the center for all fields
widget.setAlignment(Qt.AlignCenter)

layout.addWidget(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
  • updateDividendPeriod: Calculates and updates the dividend period based on the transaction date.
    def updateDividendPeriod(self, date_str):
try:
date = datetime.strptime(date_str, "%Y-%m-%d")
quarter = (date.month - 1) // 3 + 1
dividend_period = f"{date.year}Q{quarter}"
self.input_fields["Dividend Period"].setText(dividend_period)
except ValueError:
# Handle invalid date format
self.input_fields["Dividend Period"].clear()
  • is_valid_date and is_valid_money_input: Validate the transaction date and money inputs.
    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 2023 <= date_obj.year and 1 <= date_obj.month <= 12 and 1 <= date_obj.day <= 31:
return True
except ValueError:
pass

return False

def is_valid_money_input(self, money_in, money_out):
# Check if both are empty or both are filled
if bool(money_in) == bool(money_out):
return False, "Please enter a value in either 'Money In' or 'Money Out', but not both."

def is_valid_number(value):
try:
# Convert to float and check for positive value
number = float(value)
if number <= 0:
return False

# Check for no more than two decimal places
if '.' in value:
decimal_part = value.split('.')[-1]
if len(decimal_part) > 2:
return False

return True
except ValueError:
return False

# Validate 'Money In' input
if money_in and not is_valid_number(money_in):
return False, "'Money In' should be a positive number with up to two decimal places."

# Validate 'Money Out' input
if money_out and not is_valid_number(money_out):
return False, "'Money Out' should be a positive number with up to two decimal places."

return True, ""
  • submit_transaction: Collects data from the form, validates it, shows a preview dialog, and processes the transaction if confirmed.
    def submit_transaction(self):

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

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

money_in = self.input_fields["Money In"].text()
money_out = self.input_fields["Money Out"].text()

is_valid, message = self.is_valid_money_input(money_in, money_out)

if not is_valid:
QMessageBox.warning(self, "Invalid Input", message)
return # Stop further processing


# Collect data from the transaction fields
transaction_input_data = {key: field.text() for key, field in self.input_fields.items()}

transaction_data = self.convert_transaction_data(transaction_input_data)

transaction_data.update(self.display_investor_data)
transaction_data.update(self.display_account_data)

# Show the preview dialog
if self.show_preview_dialog(transaction_data):
# User clicked "OK" in the preview dialog
self.process_transaction(transaction_data)
else:
# User clicked "Cancel" in the preview dialog
# Optional: Any action to be taken on cancelation
pass
  • process_transaction: Submits the transaction data to Google Sheets and handles success or error messages.
    def process_transaction(self, transaction_data):

# TODO: Validate transaction data before submission

# Submit the transaction data to Google Sheets
try:
worksheet = self.google_sheets_client.open_spreadsheet(GOOGLESHEET_ID).worksheet(self.worksheet_name)

# Get column headers from the first row of the worksheet
column_headers = worksheet.row_values(1)

# Prepare row_data according to the order of column headers
row_data = [transaction_data.get(header, "") for header in column_headers]

# Update the cells starting from the first column of the next row
worksheet.append_row(row_data)

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

self.clear_input_fields()
  • clear_input_fields: Clears all input fields after transaction submission.
    def clear_input_fields(self):
# Clear all input fields
for widget in self.input_fields.values():
widget.clear()
  • show_preview_dialog: Shows a preview dialog with the transaction details for user confirmation.
    def show_preview_dialog(self, transaction_data):
dialog = QDialog(self)
dialog.setWindowTitle("Preview")

layout = QVBoxLayout(dialog)

keys_to_keep = ["Transaction Amount", "Transaction Date", "Money In/Out", "Last Name", "First Name", "Company Name", "Reinvest/Cash Dividend"]

# Create a new dictionary with only the selected keys
transaction_data_preview = {key: transaction_data[key] for key in keys_to_keep}

# Display transaction data
for key, value in transaction_data_preview.items():
label = QLabel(f"{key}: {value}")
label.setAlignment(Qt.AlignCenter) # Align the label text to the center
layout.addWidget(label)

# Buttons
button_layout = QHBoxLayout()
ok_button = QPushButton("OK")
cancel_button = QPushButton("Cancel")
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)

ok_button.clicked.connect(dialog.accept)
cancel_button.clicked.connect(dialog.reject)

return dialog.exec() == QDialog.Accepted
  • convert_transaction_data: Converts the collected transaction data into the format required for submission.
    def convert_transaction_data(self, original_data):
result_data = {
"Money In/Out": None,
"Amount": None,
"Transaction Amount": None,
"Transaction Date": None
}

money_in = original_data.get("Money In")
money_out = original_data.get("Money Out")
transaction_date = original_data.get("Transaction Date")

# Determine "Money In/Out" and "Amount" based on the rules
if money_in:
result_data["Money In/Out"] = "Money In"
result_data["Amount"] = money_in
result_data["Transaction Amount"] = float(money_in)
elif money_out:
result_data["Money In/Out"] = "Money Out"
result_data["Amount"] = money_out
result_data["Transaction Amount"] = -float(money_out)

# Copy "Transaction Date"
result_data["Transaction Date"] = transaction_date

return result_data

--

--

Henry Wu
Henry Wu

Written by Henry Wu

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