Пост от 16.12.2024, 05:06:00
Отредактирован 16.02.2026, 19:33:31
Время чтения: 5мин.
Каждый год Томский Кванториум проводит конкурс программирования, в данном случае речь пойдёт о “Соревнование «Coding Fest. Семейный код» 2024”. Я с моей командой выбрали трек разработки приложения для настольных компьютеров.
Задача стояла:
Кейс «Приложение «Семейный бюджет»
Цель: создать приложения на ПК для ведения семейного бюджета с возможностью добавления доходов и расходов.
Функционал: в приложении должна быть реализована возможность вводить следующие данные: дата, сумма дохода, сумма расхода. Введенные данные заносятся в таблицу Excel, где в 1 столбце – дата, во 2 – сумма дохода, в 3 – сумма расходов, а в 4 должна вычисляться оставшаяся сумма. В приложении нужно добавить возможность вывести оставшуюся сумму, сумму доходов, сумму расходов за определенный промежуток времени и за все время.
Язык программирования: Python
Отправка работ: необходимо отправить архив, в названии которого нужно указать название вашей команды. В архиве должен быть файл с кодом и все файлы необходимые для его работы, в том числе файл Excel, а также текстовый файл, в котором будет описан его функционал и при необходимости краткая инструкция пользования.Полезные ссылки:
- Мастер класс: Профессия «Финансовый разработчик»: http://technopredki.ru/page56296793.html
- Работа с библиотекой tkinter:https://python-scripts.com/tkinter
Критерий Описание Баллы Функциональность Выполняет ли приложение все базовые функции? 0-20 Интуитивность интерфейса Насколько легко пользователям ориентироваться в приложении? 0-15 Удобство использования Насколько удобно добавлять,редактировать и удалять записи? 0-15 Работа с данными Поддерживает ли приложение импорт/экспорт данных Excel? 0-10 Отчетность и визуализация Представляет ли приложение информацию о бюджете в виде графиков и отчетов? 0-10 Обработка ошибок Как приложение обрабатывает некорректные данные или ошибки ввода? 0-10 Производительность Насколько быстро приложение работает (добавление/удаление записи, расчет и т.д.)? 0-10 Эстетика интерфейса Нравится ли внешний вид приложения (дизайн, шрифты, цветовая палитра)? 0-5 Дополнительные баллы Присуждаются по желанию эксперта 0-5
Наша команда составляла из меня, вашего слуги “не особо покорного))))”, Vasulio Игорь. Два этих прогера производили тестирование, программирование некоторых вариантов отчётов, помогали с идеями и дизайном интерфейса.
Начала было положено выбором стэка, т.к. даже у организаторов была программа на Pandas, он и был выбран в качестве движка для программы. Для интерфейса же изначально был выбран tkinter.
Сначала мы начали выводом таблицы и разводкой интерфейса на удобные части. В результате этого процесса я заметил, что у tkinter не имеется удобного меню для выбора даты, и то, что у меня больше опыта в QT не делало разработку быстрее. Поэтому я провёл максимально быстрое мигрирование на pySide6(PyQT).

В результате первого дня был сделан первый набросок интерфейса, импорт/экспорт(без проверки, она будет дальше), добавление и редактирование записей в отдельной вкладке
import sysimport datetimefrom dateutil import parser
import pandasfrom pandas.tseries import offsets
from PySide6.QtWidgets import QApplication, QDialog, QMainWindow, QPushButton, QDialogButtonBox, QVBoxLayout, QLabel, QTableWidget, QMenu,QAbstractItemViewfrom PySide6.QtGui import QPalette, QColorfrom PySide6.QtWidgets import QApplication, QTableWidgetItemimport PySide6.QtCorefrom PySide6.QtCore import QDate, QRectfrom PySide6 import QtWidgetsfrom 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 день.
Теперь кнопки маленькой справки,
графика,
и вида как в электронном дневнике начали работать

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