From 1552ef2fbbaeed1a22b9788b3e8e6e0c0e18cddb Mon Sep 17 00:00:00 2001 From: anton Date: Sun, 29 Jun 2025 16:14:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A4=D0=B8=D0=BA=D1=81=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2=20=D0=B2=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=BE=D0=B7=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 116 +++++++++++++++ init_db.py | 25 ++++ requirements.txt | 4 + static/styles.css | 338 +++++++++++++++++++++++++++++++++++++++++++ templates/add.html | 60 ++++++++ templates/edit.html | 60 ++++++++ templates/index.html | 108 ++++++++++++++ 7 files changed, 711 insertions(+) create mode 100644 app.py create mode 100644 init_db.py create mode 100644 requirements.txt create mode 100644 static/styles.css create mode 100644 templates/add.html create mode 100644 templates/edit.html create mode 100644 templates/index.html diff --git a/app.py b/app.py new file mode 100644 index 0000000..35fd585 --- /dev/null +++ b/app.py @@ -0,0 +1,116 @@ +import os +from flask import Flask, render_template, request, redirect, url_for, flash +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime, date +from dotenv import load_dotenv + +load_dotenv() # Загрузка переменных окружения из .env + +app = Flask(__name__) +app.secret_key = os.getenv('SECRET_KEY', 'secret-key') + +# Конфигурация PostgreSQL +app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL', 'postgresql://user:password@localhost/finance_db') +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) + +# Модель данных +class Transaction(db.Model): + id = db.Column(db.Integer, primary_key=True) + type = db.Column(db.String(10), nullable=False) # 'income' или 'expense' + amount = db.Column(db.Float, nullable=False) + category = db.Column(db.String(50), nullable=False) + date = db.Column(db.Date, nullable=False, default=date.today()) + description = db.Column(db.Text) + + def __repr__(self): + return f'' + +# Роуты +@app.route('/') +def index(): + # Получаем все транзакции, отсортированные по дате + transactions = Transaction.query.order_by(Transaction.date.desc()).all() + + # Рассчитываем общие суммы + total_income = sum(t.amount for t in transactions if t.type == 'income') + total_expense = sum(t.amount for t in transactions if t.type == 'expense') + balance = total_income - total_expense + + # Группируем по категориям + categories = {} + for t in transactions: + if t.category not in categories: + categories[t.category] = {'income': 0, 'expense': 0} + categories[t.category][t.type] += t.amount + + return render_template('index.html', + transactions=transactions, + total_income=total_income, + total_expense=total_expense, + balance=balance, + categories=categories, + current_date=date.today()) + +@app.route('/add', methods=['GET', 'POST']) +def add_transaction(): + if request.method == 'POST': + # Получаем данные из формы + transaction_type = request.form['type'] + amount = float(request.form['amount']) + category = request.form['category'] + transaction_date = datetime.strptime(request.form['date'], '%Y-%m-%d').date() + description = request.form['description'] + + # Создаем новую транзакцию + new_transaction = Transaction( + type=transaction_type, + amount=amount, + category=category, + date=transaction_date, + description=description + ) + + # Сохраняем в БД + db.session.add(new_transaction) + db.session.commit() + + flash('Транзакция успешно добавлена!', 'success') + return redirect(url_for('index')) + + # Для GET-запроса показываем форму + return render_template('add.html', current_date=date.today()) + +@app.route('/edit/', methods=['GET', 'POST']) +def edit_transaction(id): + transaction = Transaction.query.get_or_404(id) + + if request.method == 'POST': + # Обновляем данные + transaction.type = request.form['type'] + transaction.amount = float(request.form['amount']) + transaction.category = request.form['category'] + transaction.date = datetime.strptime(request.form['date'], '%Y-%m-%d').date() + transaction.description = request.form['description'] + + # Сохраняем изменения + db.session.commit() + + flash('Транзакция успешно обновлена!', 'success') + return redirect(url_for('index')) + + # Для GET-запроса показываем форму с текущими данными + return render_template('edit.html', transaction=transaction) + +@app.route('/delete/') +def delete_transaction(id): + transaction = Transaction.query.get_or_404(id) + db.session.delete(transaction) + db.session.commit() + + flash('Транзакция успешно удалена!', 'success') + return redirect(url_for('index')) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/init_db.py b/init_db.py new file mode 100644 index 0000000..13a0f6a --- /dev/null +++ b/init_db.py @@ -0,0 +1,25 @@ +import os +from app import db, app, Transaction +from datetime import date + +app.app_context().push() + +# Создание таблиц +db.create_all() + +# Добавление начальных данных +if not Transaction.query.first(): + transactions = [ + Transaction(type='income', amount=50000, category='Зарплата', date=date(2023, 10, 15), description='Основная зарплата'), + Transaction(type='expense', amount=15000, category='Аренда', date=date(2023, 10, 5), description='Аренда квартиры'), + Transaction(type='expense', amount=3500, category='Продукты', date=date(2023, 10, 12), description='Супермаркет'), + Transaction(type='expense', amount=2500, category='Транспорт', date=date(2023, 10, 18), description='Бензин'), + Transaction(type='income', amount=10000, category='Фриланс', date=date(2023, 10, 20), description='Веб-разработка') + ] + + for t in transactions: + db.session.add(t) + db.session.commit() + print("База данных инициализирована с тестовыми данными") +else: + print("База данных уже содержит данные") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..809d97e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +flask-sqlalchemy +psycopg2-binary +python-dotenv \ No newline at end of file diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..0c8f85e --- /dev/null +++ b/static/styles.css @@ -0,0 +1,338 @@ +:root { + --primary: #2c3e50; + --secondary: #3498db; + --income: #2ecc71; + --expense: #e74c3c; + --light: #ecf0f1; + --dark: #34495e; + --gray: #7f8c8d; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; +} + +body { + background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c); + color: var(--dark); + padding: 20px; + min-height: 100vh; +} + +.container { + background: white; + border-radius: 15px; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); + overflow: hidden; + max-width: 1200px; + margin: 0 auto; +} + +.form-container { + max-width: 600px; + padding: 30px; +} + +header { + background: var(--primary); + color: white; + padding: 20px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 15px; +} + +header h1 { + display: flex; + align-items: center; + gap: 15px; + font-size: 1.8rem; +} + +.summary { + display: flex; + gap: 20px; + margin-top: 15px; + width: 100%; +} + +.summary-item { + flex: 1; + background: white; + border-radius: 10px; + padding: 15px; + text-align: center; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); +} + +.summary-item.income { + border-top: 5px solid var(--income); +} + +.summary-item.expense { + border-top: 5px solid var(--expense); +} + +.summary-item.balance { + border-top: 5px solid var(--secondary); +} + +.summary-item span { + display: block; + color: var(--gray); + font-size: 0.9rem; + margin-bottom: 5px; +} + +.summary-item strong { + font-size: 1.5rem; + display: block; +} + +.controls { + padding: 20px; + background: var(--light); + border-bottom: 1px solid #ddd; +} + +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 20px; + border-radius: 50px; + text-decoration: none; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; + border: none; + font-size: 1rem; +} + +.add-btn { + background: var(--secondary); + color: white; +} + +.add-btn:hover { + background: #2980b9; +} + +.back-btn { + background: var(--gray); + color: white; +} + +.save-btn { + background: var(--income); + color: white; + padding: 12px 25px; + font-size: 1.1rem; +} + +.back-btn:hover { + background: #666; +} + +section { + padding: 20px; +} + +h2 { + margin-bottom: 20px; + color: var(--primary); + border-bottom: 2px solid var(--light); + padding-bottom: 10px; +} + +.category-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 15px; + margin-bottom: 30px; +} + +.category-item { + background: white; + border-radius: 10px; + padding: 15px; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); + display: flex; + justify-content: space-between; + align-items: center; + border-left: 5px solid var(--expense); +} + +.category-item span.amount { + font-weight: bold; + font-size: 1.2rem; +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} + +table th, table td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #eee; +} + +table th { + background: var(--primary); + color: white; + font-weight: 500; +} + +table tbody tr:hover { + background: rgba(52, 152, 219, 0.05); +} + +table .income { + color: var(--income); + font-weight: bold; +} + +table .expense { + color: var(--expense); + font-weight: bold; +} + +.actions { + display: flex; + gap: 10px; +} + +.btn.edit-btn { + background: #f39c12; + color: white; + padding: 8px 12px; +} + +.btn.delete-btn { + background: var(--expense); + color: white; + padding: 8px 12px; +} + +.btn.edit-btn:hover { + background: #e67e22; +} + +.btn.delete-btn:hover { + background: #c0392b; +} + +.empty { + text-align: center; + padding: 20px; + color: var(--gray); + font-style: italic; +} + +footer { + text-align: center; + padding: 20px; + color: var(--gray); + font-size: 0.9rem; + border-top: 1px solid #eee; +} + +.tip { + font-style: italic; + margin-top: 10px; + color: var(--primary); +} + +/* Формы */ +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: var(--dark); +} + +.form-group input, +.form-group textarea, +.form-group select { + width: 100%; + padding: 12px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 1rem; +} + +.form-group textarea { + min-height: 100px; + resize: vertical; +} + +.radio-group { + display: flex; + gap: 15px; +} + +.radio-label { + display: flex; + align-items: center; + cursor: pointer; +} + +.radio-custom { + display: inline-block; + padding: 10px 20px; + border-radius: 50px; + font-weight: 500; + transition: all 0.3s ease; +} + +.radio-custom.income { + background: rgba(46, 204, 113, 0.2); + color: var(--income); +} + +.radio-custom.expense { + background: rgba(231, 76, 60, 0.2); + color: var(--expense); +} + +input[type="radio"] { + display: none; +} + +input[type="radio"]:checked + .radio-custom.income { + background: var(--income); + color: white; +} + +input[type="radio"]:checked + .radio-custom.expense { + background: var(--expense); + color: white; +} + +/* Адаптивность */ +@media (max-width: 768px) { + .summary { + flex-direction: column; + } + + table { + display: block; + overflow-x: auto; + } + + .category-list { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/templates/add.html b/templates/add.html new file mode 100644 index 0000000..d3c89ad --- /dev/null +++ b/templates/add.html @@ -0,0 +1,60 @@ + + + + + + Добавить операцию + + + + +
+
+

Добавить операцию

+ + Назад + +
+ +
+
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + \ No newline at end of file diff --git a/templates/edit.html b/templates/edit.html new file mode 100644 index 0000000..f6182a2 --- /dev/null +++ b/templates/edit.html @@ -0,0 +1,60 @@ + + + + + + Редактировать операцию + + + + +
+
+

Редактировать операцию

+ + Назад + +
+ +
+
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+ + \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..7ec9555 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,108 @@ + + + + + + Финансовый учет + + + + +
+
+

Финансовый учет

+
+
+ Доходы + {{ total_income|int }} ₽ +
+
+ Расходы + {{ total_expense|int }} ₽ +
+
+ Баланс + {{ balance|int }} ₽ +
+
+
+ + + +
+

Расходы по категориям

+
+ {% for category, amounts in categories.items() %} + {% if amounts['expense'] > 0 %} +
+ {{ category }} + {{ amounts['expense']|int }} ₽ +
+ {% endif %} + {% endfor %} +
+
+ +
+

История операций

+ + + + + + + + + + + + {% for t in transactions %} + + + + + + + + {% else %} + + + + {% endfor %} + +
ДатаКатегорияОписаниеСуммаДействия
{{ t.date.strftime('%d.%m.%Y') }}{{ t.category }}{{ t.description }} + {{ t.amount|int }} ₽ + + + + + + + +
Нет операций
+
+ +
+

Сегодня: {{ current_date.strftime('%d.%m.%Y') }}

+

"Учет финансов - первый шаг к финансовой свободе!"

+
+
+ + + + + \ No newline at end of file