How to use Python, PySide6 and Google Sheets to make a Database App (5): Add Transactions

Henry Coder
hellocode
Published in
6 min readDec 18, 2023

This part is about building the Transaction part.

First, handles user interactions with the menu in the application:


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


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()
elif top_level_item == "Accounts" and second_level_item == "Add Account":
self.display_add_account_form()
elif top_level_item == "Transactions" and second_level_item == "Add Transaction":
self.display_transaction_form()

Display Transaction Form

Since the transaction is based on an account, the next thing after the user clicks the menu is to pop up an account list.

    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)

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


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", "Bank Name", "Branch Number", "Account Number"]
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

def showTransactionForm(self, account_data):
# Create the TransactionForm with the fetched account data
transaction_form = TransactionForm(self.google_sheets_client, account_data)
self.content.layout().addWidget(transaction_form)

TransactionForm Class

Purpose: This class creates a form for adding a transaction.

  • Initialization (__init__):
  • Parameters:
  • google_sheets_client: An object to interact with Google Sheets.
  • account_data: A dictionary containing data related to a specific account.
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 = ["Money In:", "Money Out:", "Transaction Date:"]
self.transaction_fields = ["Money In", "Money Out", "Transaction Date"]

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

# Initialize UI
self.initUI()

Functionality:

  • Extracts relevant investor and account information from account_data.
  • Initializes labels and fields for displaying investor and account information, and for inputting transaction details.
  • Calls initUI to set up the user interface.

initUI Method:

  • Sets up the layout and user interface elements of the form.
  • Creates a scrollable area with group boxes for displaying investor and account information, and a separate group box for transaction details.
  • Adds a submit button for submitting the transaction data.
class TransactionForm(QWidget):
def __init__(self, google_sheets_client, account_data):
# ...

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 Method:

  • Creates and returns a QGroupBox with a specified title.
  • Depending on the is_editable flag, it adds either read-only line edits with existing data or empty line edits for new input.
  • Arranges the labels and line edits in a grid layout within the group box.
class TransactionForm(QWidget):
def __init__(self, google_sheets_client, account_data):
# ...

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;")

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

submit_transaction Method:

  • Gathers the data entered in the transaction fields.
  • Updates this data with the investor and account information.
  • Attempts to submit this combined data to a specific worksheet in Google Sheets.
  • Clears the input fields after submission or shows an error message if the submission fails.
class TransactionForm(QWidget):
def __init__(self, google_sheets_client, account_data):
# ...

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


transaction_data.update(self.display_investor_data)
transaction_data.update(self.display_account_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()

def clear_input_fields(self):
# Clear all input fields
for widget in self.input_fields.values():
widget.clear()

SelectAccountDialog Class

Purpose: Creates a dialog to select an account from a list.

  • Initialization (__init__):

Parameters:

  • google_sheets_client: An object to interact with Google Sheets.

Functionality:

  • Sets up the window title, size, and layout.
  • Initializes a table to display account information.
  • Adds “OK” and “Cancel” buttons for user actions.
class SelectAccountDialog(QDialog):
def __init__(self, google_sheets_client):
super().__init__()

self.google_sheets_client = google_sheets_client
self.selected_account_id = None

self.setWindowTitle("Select Account")
self.setGeometry(100, 100, 600, 400)

self.desired_columns = ["ID_Account", "ID_Investor", "Type", "Last Name", "First Name", "Company Name", "Bank Name", "Branch Number", "Account Number"]

# Center the window on the screen
self.center_window()

self.initUI()


def center_window(self):
# Get the primary screen
screen = QApplication.primaryScreen()

# Get the size of the screen
screen_geometry = screen.geometry()

# Calculate the position to center the window
x = (screen_geometry.width() - self.width()) // 2
y = (screen_geometry.height() - self.height()) // 2

# Set the position
self.move(x, y)

initUI Method:

  • Configures the user interface of the dialog.
  • Sets up a table to list accounts, fetching data from a Google Sheet.
  • Configures the table to be non-editable and to allow selection of rows.
  • Defines the actions for double-clicking a row and clicking the “OK” or “Cancel” buttons.
class SelectAccountDialog(QDialog):
def __init__(self, google_sheets_client):
#...


def initUI(self):
layout = QVBoxLayout(self)

# Table to display accounts
self.table = QTableWidget()
self.table.setSelectionBehavior(QTableWidget.SelectRows) # Select entire rows
self.table.setSelectionMode(QTableWidget.SingleSelection) # Allow only single row selection
self.table.setEditTriggers(QTableWidget.NoEditTriggers) # Make table content non-editable
self.table.itemDoubleClicked.connect(self.onRowDoubleClicked)

self.table.setColumnCount(len(self.desired_columns))
self.table.setHorizontalHeaderLabels(self.desired_columns)
self.loadAccountsData()

# Buttons
btn_layout = QHBoxLayout()
ok_btn = QPushButton("OK")
ok_btn.setFixedWidth(100)
ok_btn.clicked.connect(self.accept)

cancel_btn = QPushButton("Cancel")
cancel_btn.setFixedWidth(100)
cancel_btn.clicked.connect(self.reject)

btn_layout.addWidget(ok_btn)
btn_layout.addWidget(cancel_btn)

layout.addWidget(self.table)
layout.addLayout(btn_layout)

def onRowDoubleClicked(self, item):
# This method gets triggered on double-clicking a table row
# Perform the same action as clicking the "OK" button
self.accept()

loadAccountsData Method:

  • Fetches and displays account data from the Google Sheets in the table.
  • Maps column names to indices and checks if all desired columns are present.
  • Inserts the fetched data into the table.
class SelectAccountDialog(QDialog):
def __init__(self, google_sheets_client):
#...
def loadAccountsData(self):
# Fetch data from Google Sheets and identify column indices
worksheet = self.google_sheets_client.open_spreadsheet(GOOGLESHEET_ID).worksheet('02_Accounts')
all_data = worksheet.get_all_values()
header_row = all_data[0] # The first row is assumed to be the header

# Create a dictionary to map column names to their indices
column_indices = {name: index for index, name in enumerate(header_row)}

# Define the desired column names
desired_columns = self.desired_columns

# Check if all desired columns are present in the sheet
for col in desired_columns:
if col not in column_indices:
print(f"Column '{col}' not found in the spreadsheet.")
return

# Populate the table with data
for row_data in all_data[1:]:
row = self.table.rowCount()
self.table.insertRow(row)
for col_name in desired_columns:
col_index = column_indices[col_name]
self.table.setItem(row, desired_columns.index(col_name), QTableWidgetItem(row_data[col_index]))

def getSelectedAccountID(self):
# Return the selected account's ID
selected_row = self.table.currentRow()
if selected_row != -1:
return self.table.item(selected_row, 0).text()
return None

--

--