Пост от 16.12.2024, 05:06:00

Отредактирован 16.02.2026, 19:33:31

Категории: programming

Время чтения: 5мин.

Введение

Каждый год Томский Кванториум проводит конкурс программирования, в данном случае речь пойдёт о “Соревнование «Coding Fest. Семейный код» 2024”. Я с моей командой выбрали трек разработки приложения для настольных компьютеров.

Задача стояла:

Кейс «Приложение «Семейный бюджет»
Цель: создать приложения на ПК для ведения семейного бюджета с возможностью добавления доходов и расходов.
Функционал: в приложении должна быть реализована возможность вводить следующие данные: дата, сумма дохода, сумма расхода. Введенные данные заносятся в таблицу Excel, где в 1 столбце – дата, во 2 – сумма дохода, в 3 – сумма расходов, а в 4 должна вычисляться оставшаяся сумма. В приложении нужно добавить возможность вывести оставшуюся сумму, сумму доходов, сумму расходов за определенный промежуток времени и за все время.
Язык программирования: Python
Отправка работ: необходимо отправить архив, в названии которого нужно указать название вашей команды. В архиве должен быть файл с кодом и все файлы необходимые для его работы, в том числе файл Excel, а также текстовый файл, в котором будет описан его функционал и при необходимости краткая инструкция пользования.

Полезные ссылки:

КритерийОписаниеБаллы
ФункциональностьВыполняет ли приложение все базовые функции?0-20
Интуитивность интерфейсаНасколько легко пользователям ориентироваться в приложении?0-15
Удобство использованияНасколько удобно добавлять,редактировать и удалять записи?0-15
Работа с даннымиПоддерживает ли приложение импорт/экспорт данных Excel?0-10
Отчетность и визуализацияПредставляет ли приложение информацию о бюджете в виде графиков и отчетов?0-10
Обработка ошибокКак приложение обрабатывает некорректные данные или ошибки ввода?0-10
ПроизводительностьНасколько быстро приложение работает (добавление/удаление записи, расчет и т.д.)?0-10
Эстетика интерфейсаНравится ли внешний вид приложения (дизайн, шрифты, цветовая палитра)?0-5
Дополнительные баллыПрисуждаются по желанию эксперта0-5
А и да..... Для всего проекта было дано 3 дня

Наша команда составляла из меня, вашего слуги “не особо покорного))))”, Vasulio Игорь. Два этих прогера производили тестирование, программирование некоторых вариантов отчётов, помогали с идеями и дизайном интерфейса.

Начало программирования

Начала было положено выбором стэка, т.к. даже у организаторов была программа на Pandas, он и был выбран в качестве движка для программы. Для интерфейса же изначально был выбран tkinter.

Сначала мы начали выводом таблицы и разводкой интерфейса на удобные части. В результате этого процесса я заметил, что у tkinter не имеется удобного меню для выбора даты, и то, что у меня больше опыта в QT не делало разработку быстрее. Поэтому я провёл максимально быстрое мигрирование на pySide6(PyQT).

Первая версия интерфейса

В результате первого дня был сделан первый набросок интерфейса, импорт/экспорт(без проверки, она будет дальше), добавление и редактирование записей в отдельной вкладке

Код
import sys
import datetime
from dateutil import parser
import pandas
from pandas.tseries import offsets
from PySide6.QtWidgets import QApplication, QDialog, QMainWindow, QPushButton, QDialogButtonBox, QVBoxLayout, QLabel, QTableWidget, QMenu,QAbstractItemView
from PySide6.QtGui import QPalette, QColor
from PySide6.QtWidgets import QApplication, QTableWidgetItem
import PySide6.QtCore
from PySide6.QtCore import QDate, QRect
from PySide6 import QtWidgets
from PySide6.QtUiTools import QUiLoader
expenses = pandas.read_excel("FamilyBudget.xlsx")
expenses = expenses.sort_values("Дата", ascending=False)
expenses["Оставшаяся сумма"] = expenses["Сумма дохода"] - \
expenses["Сумма расхода"]
expenses = expenses.groupby('Дата').sum().reset_index()
print(expenses.groupby('Дата').sum())
this_year = datetime.date.today() - offsets.YearBegin()
this_month = datetime.date.today() - offsets.MonthBegin()
today = datetime.datetime.today()+datetime.timedelta(days=1)
first = today.replace(day=1)
last_month = first - datetime.timedelta(days=1)
first_day_of_current_month = datetime.date.today().replace(day=1)
last_day_of_previous_month = first_day_of_current_month - \
datetime.timedelta(days=1)
previous_year = datetime.datetime(today.year - 1, 1, 1)
def update_set_ui(currentRow, currentColumn, previousRow, previousColumn):
print(currentRow, currentColumn, previousRow, previousColumn)
def button_clicked(window):
start = datetime.datetime.combine(
window.StartDate.date().toPython(), datetime.datetime.min.time())
end = datetime.datetime.combine(
window.EndDate.date().toPython(), datetime.datetime.min.time())
summ = expenses.loc[(start < expenses["Дата"]) & (
expenses["Дата"] < end), "Сумма дохода"].sum()
summ_expens = expenses.loc[(start < expenses["Дата"]) & (
expenses["Дата"] < end), "Сумма расхода"].sum()
summ_res = expenses.loc[(start < expenses["Дата"]) & (
expenses["Дата"] < end), "Оставшаяся сумма"].sum()
dlg = CustomDialog(start, end, window, summ, summ_expens, summ_res)
if dlg.exec():
print("Success!")
else:
print("Cancel!")
def redact_element(window,):
pass
class MyWindow(QTableWidget):
def __init__(self,parent):
super().__init__(parent)
# Create the context menu and add some actions
self.context_menu = QMenu(self)
action1 = self.context_menu.addAction("Изменить")
action2 = self.context_menu.addAction("Удалить")
# Connect the actions to methods
action1.triggered.connect(self.action1_triggered)
action2.triggered.connect(self.action2_triggered)
self.show()
def contextMenuEvent(self, event):
# Show the context menu
self.context_menu.exec(event.globalPos())
def action1_triggered(self):
# Handle the "Action 1" action
pass
def action2_triggered(self):
global expenses
print(super().currentRow())
print(parser.parse(super().itemAt(0, super().currentRow()).text()))
expenses = expenses.drop(expenses.isin([parser.parse(super().itemAt(0, super().currentRow()).text())]).any(axis=1).idxmax())
self.render_table()
# Handle the "Action 2" action
pass
def render_table(self):
global expenses
expenses_p = expenses.values.tolist()
super().setRowCount(len(expenses_p))
for y in range(len(expenses_p)):
for x in range(4):
super().setItem(
y, x, QTableWidgetItem(str(expenses_p[y][x])))
class CustomDialog(QDialog):
def __init__(self, start, end, parent=None, incomes=0, expenses=0, results=0):
super().__init__(parent)
self.setWindowTitle("Отчёт по дате")
QBtn = (
QDialogButtonBox.StandardButton.Ok)
self.buttonBox = QDialogButtonBox(QBtn)
self.buttonBox.accepted.connect(self.accept)
self.buttonBox.rejected.connect(self.reject)
layout = QVBoxLayout()
message = QLabel(f"За промежуток с {start} до {end}:\n доходов на {incomes} расходов на {
expenses}\nи итоговый бюджет на {results} рублей.")
layout.addWidget(message)
layout.addWidget(self.buttonBox)
self.setLayout(layout)
def submit_info(window, date, plus, minus):
add_new_thing(date, plus, minus)
render_table(window)
def add_new_thing(date, plus, minus):
global expenses
expenses.loc[len(expenses)] = [datetime.datetime.combine(
date.toPython(), datetime.datetime.min.time()), plus, minus, 0]
def render_table(window):
expenses_p = expenses.values.tolist()
window.budgetTable.setRowCount(len(expenses_p))
for y in range(len(expenses_p)):
for x in range(4):
window.budgetTable.setItem(
y, x, QTableWidgetItem(str(expenses_p[y][x])))
def set_time_using_choice(window):
times = {0: (this_month, today),
1: (last_month, this_month),
2: (this_year, today),
3: (previous_year, this_year),
4: (expenses["Дата"].min(), expenses["Дата"].max())}
choice = window.ChosedPeriod.currentIndex()
window.StartDate.setDate(QDate(times[choice][0]))
window.EndDate.setDate(QDate(times[choice][1]))
if __name__ == "__main__":
app = QApplication(sys.argv)
ui_file_name = "main.ui"
ui_file = PySide6.QtCore.QFile(ui_file_name)
if not ui_file.open(PySide6.QtCore.QFile.ReadOnly):
print(f"Cannot open {ui_file_name}: {ui_file.errorString()}")
sys.exit(-1)
loader = QUiLoader()
window = loader.load(ui_file)
ui_file.close()
window.setFixedSize(900, 700)
set_time_using_choice(window)
window.dateEdit.setDate(QDate(datetime.date.today()))
window.AddValue.clicked.connect(lambda: submit_info(window,
window.dateEdit.date(), window.ExpenseValue.value(), window.IncomeValue.value()))
window.ApplyPeriodButton.clicked.connect(
lambda: set_time_using_choice(window))
window.MakeReport.clicked.connect(lambda: button_clicked(window))
window.DeleteButton.clicked.connect(lambda: window.budgetTable.action2_triggered())
window.budgetTable = MyWindow(window.centralwidget)
if (window.budgetTable.columnCount() < 4):
window.budgetTable.setColumnCount(4)
__qtablewidgetitem = QTableWidgetItem("Дата")
window.budgetTable.setHorizontalHeaderItem(0, __qtablewidgetitem)
__qtablewidgetitem1 = QTableWidgetItem("Доходы")
window.budgetTable.setHorizontalHeaderItem(1, __qtablewidgetitem1)
__qtablewidgetitem2 = QTableWidgetItem("Расходы")
window.budgetTable.setHorizontalHeaderItem(2, __qtablewidgetitem2)
__qtablewidgetitem3 = QTableWidgetItem("Разница")
window.budgetTable.setHorizontalHeaderItem(3, __qtablewidgetitem3)
window.budgetTable.setObjectName(u"budgetTable")
window.budgetTable.setGeometry(QRect(10, 150, 891, 501))
window.budgetTable.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
window.budgetTable.setSortingEnabled(True)
window.budgetTable.setColumnCount(4)
window.budgetTable.horizontalHeader().setCascadingSectionResizes(True)
window.budgetTable.horizontalHeader().setDefaultSectionSize(180)
window.budgetTable.horizontalHeader().setProperty(u"showSortIndicator", True)
window.budgetTable.horizontalHeader().setStretchLastSection(True)
window.budgetTable.verticalHeader().setVisible(False)
window.budgetTable.verticalHeader().setCascadingSectionResizes(False)
window.budgetTable.verticalHeader().setMinimumSectionSize(40)
window.budgetTable.verticalHeader().setHighlightSections(False)
window.budgetTable.verticalHeader().setProperty(u"showSortIndicator", False)
window.budgetTable.verticalHeader().setStretchLastSection(False)
window.budgetTable.setAlternatingRowColors(True)
window.budgetTable.setColumnCount(4)
window.budgetTable.setSelectionBehavior(QtWidgets.QTableView.SelectRows)
app.aboutToQuit.connect(lambda:
expenses.to_excel("FamilyBudget.xlsx", index=False))
render_table(window)
if not window:
print(loader.errorString())
sys.exit(-1)
window.show()
sys.exit(app.exec())

Второй день начался с создания и исправления логики и продумывания интерфейса. И большого рефактора, конечно.

Второй день

Отчёты в программе

3 день.

Теперь кнопки маленькой справки, Report 1 графика, Report 2 и вида как в электронном дневнике начали работать Report 3

Тестирование

Наша команда была из 3 человек, именно в последний день я фиксил оставшиеся баги, отчёты.

Конец

В итоге мы командой T0MT1T заняли первое место в том соревновании, с того момента уже прошёл 1 год, как же время летит.

Вот версия с соревнования, можно также поставить другую ветку, которую я сделал с рефактором ручным и автоматизированным, решил попробовать ради прикола как автоматизированный работает, крайне разочарован, не буду больше никогда его использовать.