Finance Manager Example - Part 1¶
This example represents the part one of the tutorial series on creating a simple Finance Manager that allows users to manage their expenses and visualize them using a pie chart, using PySide6, SQLAlchemy, FastAPI, and Pydantic.
For more details, see the Finance Manager Tutorial - Part 1.
import sys
from pathlib import Path
from PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngine
from financemodel import FinanceModel # noqa: F401
if __name__ == '__main__':
app = QApplication(sys.argv)
QApplication.setOrganizationName("QtProject")
QApplication.setApplicationName("Finance Manager")
engine = QQmlApplicationEngine()
engine.addImportPath(Path(__file__).parent)
engine.loadFromModule("Finance", "Main")
if not engine.rootObjects():
sys.exit(-1)
exit_code = app.exec()
del engine
sys.exit(exit_code)
from datetime import datetime
from dataclasses import dataclass
from enum import IntEnum
from collections import defaultdict
from PySide6.QtCore import (QAbstractListModel, QEnum, Qt, QModelIndex, Slot,
QByteArray)
from PySide6.QtQml import QmlElement
QML_IMPORT_NAME = "Finance"
QML_IMPORT_MAJOR_VERSION = 1
@QmlElement
class FinanceModel(QAbstractListModel):
@QEnum
class FinanceRole(IntEnum):
ItemNameRole = Qt.ItemDataRole.DisplayRole
CategoryRole = Qt.ItemDataRole.UserRole
CostRole = Qt.ItemDataRole.UserRole + 1
DateRole = Qt.ItemDataRole.UserRole + 2
MonthRole = Qt.ItemDataRole.UserRole + 3
@dataclass
class Finance:
item_name: str
category: str
cost: float
date: str
@property
def month(self):
return datetime.strptime(self.date, "%d-%m-%Y").strftime("%B %Y")
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.m_finances = []
self.m_finances.append(self.Finance("Mobile Prepaid", "Electronics", 20.00, "15-02-2024"))
self.m_finances.append(self.Finance("Groceries-Feb-Week1", "Groceries", 60.75,
"16-01-2024"))
self.m_finances.append(self.Finance("Bus Ticket", "Transport", 5.50, "17-01-2024"))
self.m_finances.append(self.Finance("Book", "Education", 25.00, "18-01-2024"))
def rowCount(self, parent=QModelIndex()):
return len(self.m_finances)
def data(self, index: QModelIndex, role: int):
row = index.row()
if row < self.rowCount():
finance = self.m_finances[row]
if role == FinanceModel.FinanceRole.ItemNameRole:
return finance.item_name
if role == FinanceModel.FinanceRole.CategoryRole:
return finance.category
if role == FinanceModel.FinanceRole.CostRole:
return finance.cost
if role == FinanceModel.FinanceRole.DateRole:
return finance.date
if role == FinanceModel.FinanceRole.MonthRole:
return finance.month
return None
@Slot(result=dict)
def getCategoryData(self):
category_data = defaultdict(float)
for finance in self.m_finances:
category_data[finance.category] += finance.cost
return dict(category_data)
def roleNames(self):
roles = super().roleNames()
roles[FinanceModel.FinanceRole.ItemNameRole] = QByteArray(b"item_name")
roles[FinanceModel.FinanceRole.CategoryRole] = QByteArray(b"category")
roles[FinanceModel.FinanceRole.CostRole] = QByteArray(b"cost")
roles[FinanceModel.FinanceRole.DateRole] = QByteArray(b"date")
roles[FinanceModel.FinanceRole.MonthRole] = QByteArray(b"month")
return roles
@Slot(int, result='QVariantMap')
def get(self, row: int):
finance = self.m_finances[row]
return {"item_name": finance.item_name, "category": finance.category,
"cost": finance.cost, "date": finance.date}
@Slot(str, str, float, str)
def append(self, item_name: str, category: str, cost: float, date: str):
finance = self.Finance(item_name, category, cost, date)
self.beginInsertRows(QModelIndex(), 0, 0) # Insert at the front
self.m_finances.insert(0, finance) # Insert at the front of the list
self.endInsertRows()
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Dialog {
id: dialog
signal finished(string itemName, string category, real cost, string date)
contentItem: ColumnLayout {
id: form
spacing: 10
property alias itemName: itemName
property alias category: category
property alias cost: cost
property alias date: date
GridLayout {
columns: 2
columnSpacing: 20
rowSpacing: 10
Layout.fillWidth: true
Label {
text: qsTr("Item Name:")
Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
}
TextField {
id: itemName
focus: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
}
Label {
text: qsTr("Category:")
Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
}
TextField {
id: category
focus: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
}
Label {
text: qsTr("Cost:")
Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
}
TextField {
id: cost
focus: true
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
placeholderText: qsTr("€")
inputMethodHints: Qt.ImhFormattedNumbersOnly
}
Label {
text: qsTr("Date:")
Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
}
TextField {
id: date
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft | Qt.AlignBaseline
// placeholderText: qsTr("dd-mm-yyyy")
validator: RegularExpressionValidator { regularExpression: /^[0-3]?\d-[01]?\d-\d{4}$/ }
// code to add the - automatically
onTextChanged: {
if (date.text.length === 2 || date.text.length === 5) {
date.text += "-"
}
}
Component.onCompleted: {
var today = new Date();
var day = String(today.getDate()).padStart(2, '0');
var month = String(today.getMonth() + 1).padStart(2, '0'); // Months are zero-based
var year = today.getFullYear();
date.placeholderText = day + "-" + month + "-" + year;
}
}
}
}
function createEntry() {
form.itemName.clear()
form.category.clear()
form.cost.clear()
form.date.clear()
dialog.title = qsTr("Add Finance Item")
dialog.open()
}
x: parent.width / 2 - width / 2
y: parent.height / 2 - height / 2
focus: true
modal: true
title: qsTr("Add Finance Item")
standardButtons: Dialog.Ok | Dialog.Cancel
Component.onCompleted: {
dialog.visible = false
Qt.inputMethod.visibleChanged.connect(adjustDialogPosition)
}
function adjustDialogPosition() {
if (Qt.inputMethod.visible) {
// If the keyboard is visible, move the dialog up
dialog.y = parent.height / 4 - height / 2
} else {
// If the keyboard is not visible, center the dialog
dialog.y = parent.height / 2 - height / 2
}
}
onAccepted: {
finished(form.itemName.text, form.category.text, parseFloat(form.cost.text), form.date.text)
}
}
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Controls.Material
ItemDelegate {
id: delegate
checkable: true
width: parent.width
height: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.15 :
Math.min(window.width, window.height) * 0.1
contentItem:
RowLayout {
Label {
id: dateLabel
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.03 :
Math.min(window.width, window.height) * 0.02
text: date
elide: Text.ElideRight
Layout.fillWidth: true
Layout.preferredWidth: 1
color: Material.primaryTextColor
}
ColumnLayout {
spacing: 5
Layout.fillWidth: true
Layout.preferredWidth: 1
Label {
text: item_name
color: "#5c8540"
font.bold: true
elide: Text.ElideRight
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.03 :
Math.min(window.width, window.height) * 0.02
Layout.fillWidth: true
}
Label {
text: category
elide: Text.ElideRight
Layout.fillWidth: true
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.03 :
Math.min(window.width, window.height) * 0.02
}
}
Item {
Layout.fillWidth: true // This item will take up the remaining space
}
ColumnLayout {
spacing: 5
Layout.fillWidth: true
Layout.preferredWidth: 1
Label {
text: "you spent:"
color: "#5c8540"
elide: Text.ElideRight
Layout.fillWidth: true
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.03 :
Math.min(window.width, window.height) * 0.02
}
Label {
text: cost + "€"
elide: Text.ElideRight
Layout.fillWidth: true
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.03 :
Math.min(window.width, window.height) * 0.02
}
}
}
}
pragma ComponentBehavior: Bound
import QtQuick
import QtGraphs
import QtQuick.Controls.Material
Item {
width: Screen.width
height: Screen.height
GraphsView {
id: chart
anchors.fill: parent
antialiasing: true
theme: GraphsTheme {
colorScheme: Qt.Dark
theme: GraphsTheme.Theme.QtGreenNeon
}
PieSeries {
id: pieSeries
}
}
Text {
id: chartTitle
text: "Total Expenses Breakdown by Category"
color: "#5c8540"
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.04 :
Math.min(window.width, window.height) * 0.03
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 20
}
function updateChart(data) {
pieSeries.clear()
for (var category in data) {
var slice = pieSeries.append(category, data[category])
slice.label = category + ": " + data[category] + "€"
slice.labelVisible = true
}
}
}
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material
ListView {
id: listView
anchors.fill: parent
height: parent.height
property var financeModel
delegate: FinanceDelegate {
id: delegate
width: listView.width
}
model: financeModel
section.property: "month" // Group items by the "month" property
section.criteria: ViewSection.FullString
section.delegate: Component {
id: sectionHeading
Rectangle {
width: listView.width
height: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.05 :
Math.min(window.width, window.height) * 0.03
color: "#5c8540"
required property string section
Text {
text: parent.section
font.bold: true
// depending on the screen density, adjust the font size
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.03 :
Math.min(window.width, window.height) * 0.02
color: Material.primaryTextColor
}
}
}
ScrollBar.vertical: ScrollBar { }
}
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material
import Finance
ApplicationWindow {
id: window
Material.theme: Material.Dark
Material.accent: Material.Gray
width: Screen.width * 0.3
height: Screen.height * 0.5
visible: true
title: qsTr("Finance Manager")
// Add a toolbar for the application, only visible on mobile
header: ToolBar {
Material.primary: "#5c8540"
visible: Qt.platform.os == "android"
RowLayout {
anchors.fill: parent
Label {
text: qsTr("Finance Manager")
font.pixelSize: 20
Layout.alignment: Qt.AlignCenter
}
}
}
ColumnLayout {
anchors.fill: parent
TabBar {
id: tabBar
Layout.fillWidth: true
TabButton {
text: qsTr("Expenses")
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.04 :
Math.min(window.width, window.height) * 0.02
onClicked: stackView.currentIndex = 0
}
TabButton {
text: qsTr("Charts")
font.pixelSize: Qt.platform.os == "android" ?
Math.min(window.width, window.height) * 0.04 :
Math.min(window.width, window.height) * 0.02
onClicked: stackView.currentIndex = 1
}
}
StackLayout {
id: stackView
Layout.fillWidth: true
Layout.fillHeight: true
Item {
id: expensesView
Layout.fillWidth: true
Layout.fillHeight: true
FinanceView {
id: financeView
anchors.fill: parent
financeModel: finance_model
}
}
Item {
id: chartsView
Layout.fillWidth: true
Layout.fillHeight: true
FinancePieChart {
id: financePieChart
anchors.fill: parent
Component.onCompleted: {
var categoryData = finance_model.getCategoryData()
updateChart(categoryData)
}
}
}
}
}
// Model to store the finance data. Created from Python.
FinanceModel {
id: finance_model
}
// Add a dialog to add new entries
AddDialog {
id: addDialog
onFinished: function(item_name, category, cost, date) {
finance_model.append(item_name, category, cost, date)
var categoryData = finance_model.getCategoryData()
financePieChart.updateChart(categoryData)
}
}
// Add a button to open the dialog
ToolButton {
id: roundButton
text: qsTr("+")
highlighted: true
Material.elevation: 6
width: Qt.platform.os === "android" ?
Math.min(parent.width * 0.2, Screen.width * 0.15) :
Math.min(parent.width * 0.060, Screen.width * 0.05)
height: width // Keep the button circular
anchors.margins: 10
anchors.right: parent.right
anchors.bottom: parent.bottom
background: Rectangle {
color: "#5c8540"
radius: roundButton.width / 2
}
font.pixelSize: width * 0.4
onClicked: {
addDialog.createEntry()
}
}
}
module Finance
Main 1.0 Main.qml
FinanceView 1.0 FinanceView.qml
FinancePieChart 1.0 FinancePieChart.qml
FinanceDelegate 1.0 FinanceDelegate.qml
AddDialog 1.0 AddDialog.qml
typeinfo financemodel.qmltypes