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.
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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)
# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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.DisplayRole
CategoryRole = Qt.UserRole
CostRole = Qt.UserRole + 1
DateRole = Qt.UserRole + 2
MonthRole = Qt.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()
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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)
}
}
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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
}
}
}
}
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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
}
}
}
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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 { }
}
// Copyright (C) 2024 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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